Pregunta

I have overridden validate() in my JFrame so that I can manually control the size of a couple of nested JPanels (they're used in scrolling content onto the screen and no layout manager I know of lets you lay out components outside of the bounds of the parent container). This works fine when dragging the window to resize it, but when clicking the Maximize button, validate() gets called, the setPreferredSize()s get called, but the panel sizes don't update. Problem seen on XP, not seen in OSX.

public void validate() {
    super.validate();

    LOGGER.debug("Validate called on Frame. Resizing panel");
    if (inited == true) {
        Dimension size = panelLeft.getSize();
        int referenceHeight = size.height;
        LOGGER.info("referenceHeight is " + referenceHeight);
        size = lower.getSize();
        size.height = referenceHeight;
        lower.setPreferredSize(size);
        lower.setMinimumSize(size);
        size.height = size.height * 2;
        movingPanel.setPreferredSize(size);
        movingPanel.setMinimumSize(size);
        LOGGER.info("sizes now: panel: " + lower.getSize().height + ", scrollpane: "
                + movingPanel.getSize().height);
        if (panelSlideController != null) {
            panelSlideController.redraw();
            LOGGER.debug("redrawing panel slide controller");
        }
    }
}

panelLeft is a panel which is auto-resized by its layout manager to be the full height of the frame. So its height is used as a reference.

The relevant panels are arranged:

 ----------------------------------
|scrollPane                        |
| -------------------------------- |
||movingPanel                     ||
|| ------------------------------ ||
|||upper                         |||
|||                              |||
|||                              |||
|||                              |||
|| ------------------------------ ||
|| ------------------------------ ||
|||lower                         |||
|||                              |||
|||                              |||
|||                              |||
|| ------------------------------ ||
| -------------------------------- |
 ----------------------------------

The user only sees the top half of this layout. The movingPanel JPanel is in the viewport of a JScrollPane. The goal is to keep the upper panel taking up all the visible vertical space, so is roughly the same height as the enclosing JFrame. The lower panel is ready to scroll on and is kept with the same height as upper panel. By keeping lower.height == panelLeft.height and movingPanel.height == panelLeft.heightx2, it means half of movingPanel is showing, and upper ends at the bottom of the Frame.

Like I said, works fine. When dragging the window, some example output is:

2013-06-20 23:15:41,298 [WT-EventQueue-0] DEBUG s.billing.ui.Form  - Validate called on Frame. Resizing panel
2013-06-20 23:15:41,298 [WT-EventQueue-0] INFO  s.billing.ui.Form  - newheight is 617
2013-06-20 23:15:41,298 [WT-EventQueue-0] INFO  s.billing.ui.Form  - sizes now: panel: 607, scrollpane: 1214
2013-06-20 23:15:41,538 [WT-EventQueue-0] DEBUG s.billing.ui.Form  - Validate called on Frame. Resizing panel
2013-06-20 23:15:41,538 [WT-EventQueue-0] INFO  s.billing.ui.Form  - newheight is 640
2013-06-20 23:15:41,538 [WT-EventQueue-0] INFO  s.billing.ui.Form  - sizes now: panel: 636, scrollpane: 1272

When maximizing the window, output is like:

2013-06-20 22:08:21,234 [WT-EventQueue-0] DEBUG s.billing.ui.Form  - Validate called on Frame. Resizing panel
2013-06-20 22:08:21,234 [WT-EventQueue-0] INFO  s.billing.ui.Form  - newheight is 783
2013-06-20 22:08:21,234 [WT-EventQueue-0] INFO  s.billing.ui.Form  - sizes now: panel: 543, scrollpane: 1086

I added the setMinimumSize in there to try to be more persuasive but it didn't help.

Any thoughts very welcome

EDIT: Added SSCCE

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.ScrollPaneConstants;
import javax.swing.Timer;
import javax.swing.UIManager;

class Frame extends JFrame {

    private JPanel panelLeft;
    private JScrollPane scrollPane;
    private JPanel movingPanel;
    private JPanel upper;
    private JPanel lower;
    private boolean inited;
    private JLabel labelUpper;
    private JLabel labelLower;
    private JButton scrollBtn;
    private Frame.PanelSlideController panelSlideController;
    private JButton resizeBtn;

    private boolean lowerShowing = false;

    public Frame() {
        getContentPane().setLayout(new BorderLayout());

        panelLeft = new JPanel();
        panelLeft.setBackground(Color.CYAN);
        panelLeft.setPreferredSize(new Dimension(300, 400));
        getContentPane().add(panelLeft, BorderLayout.WEST);

        labelUpper = new JLabel("upper");
        panelLeft.add(labelUpper);

        labelLower = new JLabel("lower");
        panelLeft.add(labelLower);

        resizeBtn = new JButton("resize");
        resizeBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                doResize();
            }
        });
        panelLeft.add(resizeBtn);

        scrollBtn = new JButton("Scroll");
        scrollBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                doScroll();
            }
        });
        panelLeft.add(scrollBtn);

        scrollPane = new JScrollPane();
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);

        movingPanel = new JPanel(new GridLayout(2, 1));
        movingPanel.setOpaque(false);
        movingPanel.setPreferredSize(new Dimension(300, 400));

        upper = new JPanel();
        upper.setBackground(Color.YELLOW);
        movingPanel.add(upper);

        lower = new JPanel();
        lower.setBackground(Color.RED);
        movingPanel.add(lower);

        scrollPane.setViewportView(movingPanel);

        getContentPane().add(scrollPane, BorderLayout.EAST);

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        pack();

        inited = true;
    }

    /**
     * This is a manual step instead of overriding validate()
     */
    protected void doResize() {
        // Get the height we want
        int referenceHeight = panelLeft.getSize().height;

        // Update the height of the lower panel to equal this
        Dimension size = lower.getSize();
        size.height = referenceHeight;
        lower.setPreferredSize(size);
        lower.setMinimumSize(size);

        // Update the height of the surrounding panel
        size = scrollPane.getSize();
        size.height = referenceHeight * 2;
        movingPanel.setPreferredSize(size);
        movingPanel.setMinimumSize(size);

        if (panelSlideController != null) {
            panelSlideController.redraw();
            System.out.println("redrawing panel slide controller");
        }

        upper.invalidate();
        lower.invalidate();
        scrollPane.revalidate();
    }

    protected void doScroll() {
        panelSlideController = new PanelSlideController(scrollPane, 20);
        int scrollDirection = lowerShowing ? -1 : 1;
        panelSlideController.scrollY(panelLeft.getHeight() * scrollDirection);
        lowerShowing = !lowerShowing;
    }

    @Override
    public void validate() {
        super.validate();

        System.out.println("Validating");
        if (inited) {
            labelUpper.setText("upper: " + upper.getSize().height);
            labelLower.setText("lower: " + lower.getSize().height);
        }
    }

    class PanelSlideController implements ActionListener {

        private final JScrollPane scrollPane;
        private final int speed;
        private Timer timer;
        private int endPos;

        private boolean scrollingPositive;

        public PanelSlideController(JScrollPane scrollPane, int speed) {
            this.scrollPane = scrollPane;
            this.speed = speed;
        }

        public void scrollY(int scrollDistance) {
            endPos = scrollPane.getViewport().getViewPosition().y + scrollDistance;
            scrollingPositive = scrollDistance > 0;
            timer = new Timer(speed, this);
            timer.start();
        }

        public void redraw() {
            JViewport viewport = scrollPane.getViewport();
            Point position = viewport.getViewPosition();

            if (scrollingPositive) {
                position.y = endPos;
            }
            else {
                position.y = 0;
            }
            viewport.setViewPosition(position);

        }

        @Override
        public void actionPerformed(ActionEvent e) {
            JViewport viewport = scrollPane.getViewport();
            Point position = viewport.getViewPosition();
            int offset = scrollingPositive ? 10 : -10;
            position.y += offset;
            viewport.setViewPosition(position);

            if ((scrollingPositive && position.y >= endPos)
                    || (!scrollingPositive && (position.y <= endPos || position.y <= 0))) {
                timer.stop();
            }
        }
    }

    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
        } catch (Exception e) {
            e.printStackTrace();
        }

        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new Frame().setVisible(true);
            }
        });
    }
}

Right, so the above is runnable. panelLeft (cyan) is used as the reference height. I moved the code out of validate() and instead it's run by clicking resize button.

So you can se the red panel (lower) should not be visible until scroll is clicked, at which point yellow scrolls up while red scroll into view. And then back again in the opposite direction. To do this, I need the yellow panel taking up all the vertical height and if I can do this with a layout manager then yay.

I've updated the original snippet and the 'diagram' to mirror the names used in here.

Thanks

¿Fue útil?

Solución

OK, thanks to the prompts to use a LayoutManager, I did some digging in that direction. Turns out that JScrollPanes use JViewports which in turn use ViewportLayout implemenation of a LayoutManager. These have control over their delegated 'View' component. As can be seen in the modified code below, I override the layoutContainer method in here now to double the height of my 'movingPanel' in relation to the viewport and this works visibly better than before and when maximised in XP.

Thanks

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.ScrollPaneConstants;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.ViewportLayout;

class Frame extends JFrame {

    private JPanel panelLeft;
    private JScrollPane scrollPane;
    private JPanel movingPanel;
    private JPanel upper;
    private JPanel lower;
    private boolean inited;
    private JLabel labelUpper;
    private JLabel labelLower;
    private JButton scrollBtn;
    private Frame.PanelSlideController panelSlideController;
    private JButton resizeBtn;

    private boolean lowerShowing = false;

    public Frame() {
        getContentPane().setLayout(new BorderLayout());

        panelLeft = new JPanel();
        panelLeft.setBackground(Color.CYAN);
        panelLeft.setPreferredSize(new Dimension(300, 400));
        getContentPane().add(panelLeft, BorderLayout.WEST);

        labelUpper = new JLabel("upper");
        panelLeft.add(labelUpper);

        labelLower = new JLabel("lower");
        panelLeft.add(labelLower);

        scrollBtn = new JButton("Scroll");
        scrollBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                doScroll();
            }
        });
        panelLeft.add(scrollBtn);

        scrollPane = new JScrollPane();
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);

        movingPanel = new JPanel(new GridLayout(2, 1));
        movingPanel.setOpaque(false);
        movingPanel.setPreferredSize(new Dimension(300, 400));

        upper = new JPanel();
        upper.setBackground(Color.YELLOW);
        movingPanel.add(upper);

        lower = new JPanel();
        lower.setBackground(Color.RED);
        movingPanel.add(lower);

        // ------------------------------
        // This is the key bit
        // ------------------------------
        JViewport viewport = new JViewport() {
            @Override
            protected LayoutManager createLayoutManager() {
                return new ViewportLayout() {

                    @Override
                    public void layoutContainer(Container parent) {
                        JViewport vp = (JViewport) parent;
                        Component view = vp.getView();

                        Dimension viewPrefSize = view.getPreferredSize();
                        Dimension vpSize = vp.getSize();
                        Dimension viewSize = new Dimension(viewPrefSize);

                        viewSize.width = vpSize.width;
                        viewSize.height = vpSize.height * 2;

                        vp.setViewSize(viewSize);
                    }
                };
            }

        };
        scrollPane.setViewport(viewport);
        viewport.setView(movingPanel);
        // ------------------------------
        // End of key bit
        // ------------------------------

        getContentPane().add(scrollPane, BorderLayout.EAST);

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        pack();

        inited = true;
    }

    protected void doScroll() {
        panelSlideController = new PanelSlideController(scrollPane, 20);
        int scrollDirection = lowerShowing ? -1 : 1;
        panelSlideController.scrollY(panelLeft.getHeight() * scrollDirection);
        lowerShowing = !lowerShowing;
    }

    @Override
    public void validate() {
        super.validate();

        System.out.println("Validating");
        if (inited) {
            labelUpper.setText("upper: " + upper.getSize().height);
            labelLower.setText("lower: " + lower.getSize().height);
        }
    }

    class PanelSlideController implements ActionListener {

        private final JScrollPane scrollPane;
        private final int speed;
        private Timer timer;
        private int endPos;

        private boolean scrollingPositive;

        public PanelSlideController(JScrollPane scrollPane, int speed) {
            this.scrollPane = scrollPane;
            this.speed = speed;
        }

        public void scrollY(int scrollDistance) {
            endPos = scrollPane.getViewport().getViewPosition().y + scrollDistance;
            scrollingPositive = scrollDistance > 0;
            timer = new Timer(speed, this);
            timer.start();
        }

        public void redraw() {
            JViewport viewport = scrollPane.getViewport();
            Point position = viewport.getViewPosition();

            if (scrollingPositive) {
                position.y = endPos;
            }
            else {
                position.y = 0;
            }
            viewport.setViewPosition(position);

        }

        @Override
        public void actionPerformed(ActionEvent e) {
            JViewport viewport = scrollPane.getViewport();
            Point position = viewport.getViewPosition();
            int offset = scrollingPositive ? 10 : -10;
            position.y += offset;
            viewport.setViewPosition(position);

            if ((scrollingPositive && position.y >= endPos)
                    || (!scrollingPositive && (position.y <= endPos || position.y <= 0))) {
                timer.stop();
            }
        }
    }

    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
        } catch (Exception e) {
            e.printStackTrace();
        }

        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new Frame().setVisible(true);
            }
        });
    }
}

Note: I had a subsequent question open before deleting it, and am including the title here for SEO purposes: How to use Swing Layout Managers to contain objects out of frame?

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top