DragTabFrame closing inconsistently

2020-03-24 02:53发布

问题:

The code below is supposed to work a little like the Multi-Document Interface (MDI) you might see in a browser like FF, IE or Chrome. It presents 'documents' (a black buffered image as spacer) in a tabbed pane such that they can be dragged from the pane into a new (or existing) window by user choice.

But it has had issues with closing frames once they have no more tabs, as well as closing the JVM when there are no further visible windows. I think I fixed them by checking with a Timer in the DragTabManager:

  • It checks open frames for instances of DragTabFrame
  • If it finds one, it check the tab count. If 0 the frame is set invisible and disposed.
  • If it finds no instances of the frame that are visible, it ends the Timer to allow the JRE to exit.

At least that is how it is supposed to work. It seems to be working reliably here, and I have seen no 'empty frame or VM failing to shut down' for quite a few tests. Does it work as advertised for others, or do I need to look further?

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.EmptyBorder;

public class DragTabFrame extends JFrame {

    private JTabbedPane tabbedPane = new JTabbedPane();
    private final static DragTabManager dragTabManager = new DragTabManager();

    final MouseAdapter ma = new MouseAdapter() {

        @Override
        public void mouseClicked(MouseEvent e) {
            JComponent c = (JComponent) e.getSource();
            dragTabManager.setCurrentComponent(c);
            DragTabFrame dtf = (DragTabFrame)c.getTopLevelAncestor();
            dragTabManager.setCurrentFrame(dtf);
            JTabbedPane tp = dtf.getTabbedPane();
            int index = tp.indexOfComponent(c);
            if (index<0) index = 0;
            String title = tp.getTitleAt(index);
            dragTabManager.setCurrentTitle(title);
        }

        @Override
        public void mousePressed(MouseEvent e) {
            JComponent c = (JComponent) e.getSource();
            DragTabFrame dtf = (DragTabFrame)c.getTopLevelAncestor();
            dragTabManager.setCurrentComponent(c);
            dragTabManager.setCurrentFrame(dtf);
            JTabbedPane tp = dtf.getTabbedPane();
            int index = tp.indexOfComponent(c);
            if (index<0) index = 0;
            String title = tp.getTitleAt(index);
            dragTabManager.setCurrentTitle(title);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            JComponent c = (JComponent) e.getSource();
            if (c.getTopLevelAncestor().getBounds().contains(e.getLocationOnScreen())) {
                // do nothing, the drop point is the same frame
            } else {
                DragTabFrame dtf = getTargetFrame(e.getLocationOnScreen());
                if (dtf == null) {
                    dtf = new DragTabFrame();
                    dtf.init();
                    dtf.setLocation(e.getLocationOnScreen());
                } else {
                    DragTabFrame fromFrame = dragTabManager.getCurrentFrame();
                    fromFrame.removeTabComponent(c);
                    JTabbedPane tp = fromFrame.getTabbedPane();
                    if (tp.getTabCount() == 0) {
                        fromFrame.setVisible(false);
                        fromFrame.dispose();
                    } 
                }
                dtf.addTabComponent(dragTabManager.getCurrentTitle(), c);
                dtf.pack();
                dtf.setVisible(true);
            }
        }
    };

    public JTabbedPane getTabbedPane() {
        return tabbedPane;
    }

    public DragTabFrame getTargetFrame(Point p) {
        Frame[] frames = Frame.getFrames();
        for (Frame frame : frames) {
            if (frame instanceof DragTabFrame
                    && frame.getBounds().contains(p)) {
                return (DragTabFrame) frame;
            }
        }

        return null;
    }

    public void init() {
        // the GUI as seen by the user (without frame)
        JPanel gui = new JPanel(new BorderLayout());
        gui.setBorder(new EmptyBorder(2, 3, 2, 3));
        gui.add(tabbedPane, BorderLayout.CENTER);
        add(gui);

        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        // See https://stackoverflow.com/a/7143398/418556 for demo.
        setLocationByPlatform(true);
    }

    public void addTabComponent(String name, Component c) {
        tabbedPane.addTab(name, c);
        c.addMouseListener(ma);
        c.addMouseMotionListener(ma);
    }

    public void removeTabComponent(Component c) {
        tabbedPane.remove(c);
        c.removeMouseListener(ma);
        c.removeMouseMotionListener(ma);
    }
    public static void main(String[] args) {
        Runnable r = new Runnable() {

            @Override
            public void run() {

                DragTabFrame dtf = new DragTabFrame();
                dtf.init();
                BufferedImage bi = new BufferedImage(
                        200, 40, BufferedImage.TYPE_INT_RGB);
                for (int ii = 1; ii < 4; ii++) {
                    JLabel l = new JLabel(new ImageIcon(bi));
                    dtf.addTabComponent("Tab " + ii, l);
                }
                dtf.pack();
                // should be done last, to avoid flickering, moving,
                // resizing artifacts.
                dtf.setVisible(true);
            }
        };
        // Swing GUIs should be created and updated on the EDT
        // http://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html
        SwingUtilities.invokeLater(r);
    }
}

class DragTabManager {
    private DragTabFrame currentFrame;
    private JComponent currentComponent;
    private String currentTitle;
    private Timer timer;

    public DragTabManager() {
        ActionListener actionListener = new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                Frame[] frames = Frame.getFrames();
                if (frames.length==0) {
                    timer.stop();
                }
                System.out.println("frames.length: " + frames.length);
                boolean allInvisible = true;
                for (Frame frame : frames) {
                    if (frame instanceof DragTabFrame) {
                        DragTabFrame dtf = (DragTabFrame)frame;
                        if (dtf.isVisible()) {
                            allInvisible = false;
                        }
                        if (dtf.getTabbedPane().getTabCount()==0) {
                            dtf.setVisible(false);
                            dtf.dispose();
                        }
                    }
                }
                if (allInvisible) {
                    timer.stop();
                }
            }
        };
        timer = new Timer(200,actionListener);
        timer.start();
    }

    /**
     * @return the currentFrame
     */
    public DragTabFrame getCurrentFrame() {
        return currentFrame;
    }

    /**
     * @param currentFrame the currentFrame to set
     */
    public void setCurrentFrame(DragTabFrame currentFrame) {
        this.currentFrame = currentFrame;
    }

    /**
     * @return the currentComponent
     */
    public JComponent getCurrentComponent() {
        return currentComponent;
    }

    /**
     * @param currentComponent the currentComponent to set
     */
    public void setCurrentComponent(JComponent currentComponent) {
        this.currentComponent = currentComponent;
    }

    /**
     * @return the currentTitle
     */
    public String getCurrentTitle() {
        return currentTitle;
    }

    /**
     * @param currentTitle the currentTitle to set
     */
    public void setCurrentTitle(String currentTitle) {
        this.currentTitle = currentTitle;
    }
}

Fix

As per the suggestions in the answer of DSquare, here is the fixed source, with a few other tweaks as well.

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.EmptyBorder;

public class DragTabFrame extends JFrame {

    private JTabbedPane tabbedPane = new JTabbedPane();
    private final static DragTabManager dragTabManager = new DragTabManager();
    final MouseAdapter ma = new MouseAdapter() {

        @Override
        public void mousePressed(MouseEvent e) {
            JComponent c = (JComponent) e.getSource();
            DragTabFrame dtf = (DragTabFrame) c.getTopLevelAncestor();
            dragTabManager.setCurrentComponent(c);
            dragTabManager.setCurrentFrame(dtf);
            JTabbedPane tp = dtf.getTabbedPane();
            int index = tp.indexOfComponent(c);
            String title = tp.getTitleAt(index);
            dragTabManager.setCurrentTitle(title);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            JComponent c = (JComponent) e.getSource();
            if (c.getTopLevelAncestor().getBounds().contains(
                    e.getLocationOnScreen())) {
                // do nothing, the drop point is the same frame
            } else {
                DragTabFrame dtf = getTargetFrame(e.getLocationOnScreen());
                if (dtf == null) {
                    dtf = new DragTabFrame();
                    dtf.init();
                    dtf.setLocation(e.getLocationOnScreen());
                }
                DragTabFrame fromFrame = dragTabManager.getCurrentFrame();
                fromFrame.removeTabComponent(c);
                JTabbedPane tp = fromFrame.getTabbedPane();
                if (tp.getTabCount() == 0) {
                    fromFrame.setVisible(false);
                    fromFrame.dispose();
                }
                dtf.addTabComponent(dragTabManager.getCurrentTitle(), c);
                dtf.pack();
                dtf.setVisible(true);
            }
        }
    };

    public JTabbedPane getTabbedPane() {
        return tabbedPane;
    }

    public DragTabFrame getTargetFrame(Point p) {
        Frame[] frames = Frame.getFrames();
        for (Frame frame : frames) {
            if (frame instanceof DragTabFrame
                    && frame.getBounds().contains(p)) {
                return (DragTabFrame) frame;
            }
        }

        return null;
    }

    public void init() {
        // the GUI as seen by the user (without frame)
        JPanel gui = new JPanel(new BorderLayout());
        gui.setBorder(new EmptyBorder(2, 3, 2, 3));
        gui.add(tabbedPane, BorderLayout.CENTER);
        add(gui);

        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        // See https://stackoverflow.com/a/7143398/418556 for demo.
        setLocationByPlatform(true);
    }

    public void addTabComponent(String name, Component c) {
        tabbedPane.addTab(name, c);
        c.addMouseListener(ma);
        c.addMouseMotionListener(ma);
    }

    public void removeTabComponent(Component c) {
        tabbedPane.remove(c);
        c.removeMouseListener(ma);
        c.removeMouseMotionListener(ma);
    }

    public static void main(String[] args) {
        Runnable r = new Runnable() {

            @Override
            public void run() {

                DragTabFrame dtf = new DragTabFrame();
                dtf.init();
                BufferedImage bi = new BufferedImage(
                        200, 40, BufferedImage.TYPE_INT_RGB);
                for (int ii = 1; ii < 4; ii++) {
                    JLabel l = new JLabel(new ImageIcon(bi));
                    dtf.addTabComponent("Tab " + ii, l);
                }
                dtf.pack();
                dtf.setVisible(true);
            }
        };
        SwingUtilities.invokeLater(r);
    }
}

class DragTabManager {

    private DragTabFrame currentFrame;
    private JComponent currentComponent;
    private String currentTitle;

    /**
     * @return the currentFrame
     */
    public DragTabFrame getCurrentFrame() {
        return currentFrame;
    }

    /**
     * @param currentFrame the currentFrame to set
     */
    public void setCurrentFrame(DragTabFrame currentFrame) {
        this.currentFrame = currentFrame;
    }

    /**
     * @return the currentComponent
     */
    public JComponent getCurrentComponent() {
        return currentComponent;
    }

    /**
     * @param currentComponent the currentComponent to set
     */
    public void setCurrentComponent(JComponent currentComponent) {
        this.currentComponent = currentComponent;
    }

    /**
     * @return the currentTitle
     */
    public String getCurrentTitle() {
        return currentTitle;
    }

    /**
     * @param currentTitle the currentTitle to set
     */
    public void setCurrentTitle(String currentTitle) {
        this.currentTitle = currentTitle;
    }
}

回答1:

Deactivating the timer, the inconsistencies I found were when dragging the last tab into a new (no existing) frame. That was because of a bad placed else that didn't let the code responsible for checking and closing the original Frame to run. I've modified it like this and it works so far, without the timer:

    @Override
    public void mouseReleased(MouseEvent e) {
        JComponent c = (JComponent) e.getSource();
        if (c.getTopLevelAncestor().getBounds().contains(e.getLocationOnScreen())) {
            // do nothing, the drop point is the same frame
        } else {
            DragTabFrame dtf = getTargetFrame(e.getLocationOnScreen());
            if (dtf == null) {
                dtf = new DragTabFrame();
                dtf.init();
                dtf.setLocation(e.getLocationOnScreen());
            }// else {
            DragTabFrame fromFrame = dragTabManager.getCurrentFrame();
            fromFrame.removeTabComponent(c);
            JTabbedPane tp = fromFrame.getTabbedPane();
            if (tp.getTabCount() == 0) {
                fromFrame.setVisible(false);
                fromFrame.dispose();
            } 
            //}
            dtf.addTabComponent(dragTabManager.getCurrentTitle(), c);
            dtf.pack();
            dtf.setVisible(true);
        }
    }

[...]

public DragTabManager() {
    /* unused actionlistener code here */

    //timer = new Timer(200,actionListener);
    //timer.start();
}

I don't think there should be any other problems, though I don't know about that closing JVM problem you mentioned.