Need the height of an invalidated Swing component

2019-02-13 19:48发布

问题:

The basic setup is this: I have a vertical JSplitPane that I want to have a fixed-size bottom component and a resizing top component, which I accomplished by calling setResizeWeight(1.0). In this application there is a button to restore the "default" window configuration. The default height of the window is the desktop height, and the default divider location is 100 pixels from the bottom of the split pane.

To set the divider location to 100px, I take the JSplitPane height - 100. The problem is, just before this I resize the JFrame, and since the code is in a button callback, the JSplitPane has been invalidated but not yet resized. So the divider location is set incorrectly.

Here is a SSCCE. Click the button twice to see the problem. The first click will resize the window, but the divider location remains the same (relative to the bottom of the window). The second click properly moves the divider, since the window size didn't change.

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JSplitPane;

public class SSCCE {

    /**
     * @param args unused
     */
    public static void main(String[] args) {
        new SSCCE();
    }

    private final JFrame f = new JFrame("JSplitPane SSCE");
    private final JSplitPane sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT,true);

    public SSCCE() {
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        sp.add(new JLabel("top"));
        sp.add(new JLabel("bottom"));
        sp.setResizeWeight(1.0);

        f.getContentPane().add(sp);
        f.getContentPane().add(new JButton(new AbstractAction("Resize to Default") {
            @Override
            public void actionPerformed(ActionEvent e) {
                restoreDefaults();
            }
        }),BorderLayout.PAGE_END);

        f.setSize(400,300);
        f.setVisible(true);
    }

    void restoreDefaults() {
        f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
        sp.setDividerLocation(sp.getSize().height - 100);  // Does not work on first button press
    }

    Rectangle getDesktopRect(GraphicsConfiguration gc) {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        Dimension size = toolkit.getScreenSize();
        Insets insets = toolkit.getScreenInsets(gc);
        return new Rectangle(insets.left, insets.top, size.width - (insets.left + insets.right), size.height - (insets.top + insets.bottom));
    }

}

I have thought of a few ways I might get around this, but they all seem sort of hackish. So far the best idea I've had has been to call f.validate() in between setting the frame size and setting the divider location, but I'm concerned there might be side effects to forcing validation early.

The other option I thought of is to use EventQueue.invokeLater() to put the call to set the divider location at the end of the event queue. But that seems risky to me - I'm assuming the JSplitPane will have been validated at that point, and I'm concerned that may be a faulty assumption to make.

Is there a better way?

回答1:

Took a while (probably due to being early morning here :-) to understand the problem, so just to make sure I got it:

  • the size of the bottom component can be whatever the user decides at all times
  • when resizing the frame all height change should happen to the top component
  • there's an option to restore to default sizes, independent of any setting before
  • "default" means the bottom component must have a fixed height of xx

If so, the solution is to separate the frame resizing from the sizing the bottom component. Your second option is dead on: resize the frame and wrap the bottom comp resize into a invokeLater (EventQueue or SwingUtilities, doesn't matter).

void restoreDefaults() {
    f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            sp.setDividerLocation(sp.getSize().height - 100);  

        }
    });
}

That's guaranteed to work as expected, because the invokeLater puts the request as last after all already queued events:

 /**
 * Causes <i>doRun.run()</i> to be executed asynchronously on the
 * AWT event dispatching thread.  This will happen after all
 * pending AWT events have been processed.  [...]
 * If invokeLater is called from the event dispatching thread --
 * for example, from a JButton's ActionListener -- the <i>doRun.run()</i> will
 * still be deferred until all pending events have been processed.


回答2:

You could create a custom action class that handles the button click and the resize event. This approach would look like this:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JSplitPane;

public class SSCCE {

    /**
     * @param args unused
     */
    public static void main(String[] args) {
        new SSCCE();
    }

    private final JFrame f = new JFrame("JSplitPane SSCE");
    private final JSplitPane sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT,true);

    public SSCCE() {
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        sp.add(new JLabel("top"));
        sp.add(new JLabel("bottom"));
        sp.setResizeWeight(1.0);

        f.getContentPane().add(sp);

        CustomListener resizeViaButtonListener = new CustomListener("Resize to Default");

        f.getContentPane().add(new JButton(resizeViaButtonListener), BorderLayout.PAGE_END);
        f.addComponentListener(resizeViaButtonListener);

        f.setSize(400,300);
        f.setVisible(true);
    }

    void restoreDefaults() {
        f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
        sp.setDividerLocation(sp.getSize().height - 100);  // Does not work on first button press
    }

    Rectangle getDesktopRect(GraphicsConfiguration gc) {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        Dimension size = toolkit.getScreenSize();
        Insets insets = toolkit.getScreenInsets(gc);
        return new Rectangle(insets.left, insets.top, size.width - (insets.left + insets.right), size.height - (insets.top + insets.bottom));
    }

    class CustomListener extends AbstractAction implements ComponentListener {

        CustomListener(String actionDescription) {
            super(actionDescription);
        }

        private boolean resizedViaButtonClick = false;

        @Override
        public void actionPerformed(ActionEvent arg0) {
            resizedViaButtonClick = true;
            f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
            sp.setDividerLocation(sp.getSize().height - 100);
            // you need this also here because if the component is not resized when clicking the button
            // it is possible that the divider location must be changed. This happens when the user clicks
            // the button after changing the divider but not resizing the frame.
        }

        @Override
        public void componentResized(ComponentEvent e) {
            if ( resizedViaButtonClick ) {
                resizedViaButtonClick = false;
                sp.setDividerLocation(sp.getSize().height - 100);
            }
        }

        @Override
        public void componentHidden(ComponentEvent e) { /* do nothing */ }

        @Override
        public void componentMoved(ComponentEvent e) { /* do nothing */ }

        @Override
        public void componentShown(ComponentEvent e) { /* do nothing */ }

    }

}

This way the code that is responsible for handling the logical task of setting the standard size will be in one single and easy to understand class.



回答3:

nothing complicated, basic Swing Rules

import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.*;

public class SSCCE {

    /**
     * @param args unused
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                SSCCE sSCCE = new SSCCE();
            }
        });
    }
    private final JFrame f = new JFrame("JSplitPane SSCE");
    private final JSplitPane sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT, 
           true);

    public SSCCE() {
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        sp.add(new JLabel("top"));
        sp.add(new JLabel("bottom"));
        sp.setResizeWeight(1.0);
        f.getContentPane().add(sp);
        f.getContentPane().add(new JButton(new AbstractAction(
          "Resize to Default") {

            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println(sp.getLastDividerLocation());
                restoreDefaults();
            }
        }), BorderLayout.PAGE_END);
        f.setPreferredSize(new Dimension(400, 300));
        f.pack();
        f.setVisible(true);
    }

    void restoreDefaults() {
    //EventQueue.invokeLater(new Runnable() {
    //    @Override
    //    public void run() {
            f.setPreferredSize(new Dimension(f.getWidth(), 
                getDesktopRect(f.getGraphicsConfiguration()).height));
            f.pack();
            sp.setDividerLocation(sp.getSize().height - 100);  
                // Does not work on first button press                
    //    }
    //});
    }

    Rectangle getDesktopRect(GraphicsConfiguration gc) {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        Dimension size = toolkit.getScreenSize();
        Insets insets = toolkit.getScreenInsets(gc);
        return new Rectangle(insets.left, insets.top, 
             size.width - (insets.left + insets.right), 
             size.height - (insets.top + insets.bottom));
    }
}


回答4:

but I think pack() may be better than validate()

I generally try to avoid invoking setPreferredSize() on any component. I would rather let the layout manager do its job. In this case this would mean setting the size of the frame and let the BorderLayout take all the available space.

    void restoreDefaults() {
//        f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
        GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
        Rectangle bounds = env.getMaximumWindowBounds();
        f.setSize(f.getWidth(), bounds.height);
        f.validate();
        sp.setDividerLocation(sp.getSize().height - 100);  // Does not work on first button press
    }