Temporarily disable or prevent repainting JViewPor

2020-02-13 03:12发布

问题:

I have written a MouseListener as defined below so that I can move a JButton around to reorder the components that are within the JPanel. The JPanel is within a JScrollPane so that when multiple components are added they can be scrolled.

The problem I have is that when dragging the component and the mouse goes out of the scrollpane/viewport then the component will snap back to its position within the JPanel then will be drawn in the correct location. I assume that this behavior is due to the Viewport calling a repaint of its children when I call scrollRectToVisible()

Is there a way that I can prevent this from happening?

Please note that I am limited to Java 5

Listener

import java.awt.Component;
import java.awt.Container;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputAdapter;

public class DragListener extends MouseInputAdapter
{
    private Point location;
    private MouseEvent pressed;
    private MouseEvent dragged;
    private MouseEvent dropped;

    @Override
    public void mousePressed(MouseEvent me)
    {
        pressed = me;
    }

    @Override
    public void mouseDragged(MouseEvent me)
    {
        dragged = me;
        Component component = dragged.getComponent();
        Container parent = component.getParent();
        Container superParent = parent.getParent();

        if(superParent instanceof JViewport)
        {
            JViewport vp = (JViewport)superParent;
            Rectangle vpb = vp.getBounds();
            Point pt = MouseInfo.getPointerInfo().getLocation();
            SwingUtilities.convertPointFromScreen(pt, vp);

            if(!vpb.contains(pt))
            {
                int yDiff = (pt.y < vpb.y ) ? pt.y : pt.y - vpb.height;
                vpb.translate(0, yDiff);
                vp.scrollRectToVisible(vpb);
            }
        }

        location = component.getLocation(location);
        int x = location.x - pressed.getX() + me.getX();
        int y = location.y - pressed.getY() + me.getY();
        component.setLocation(x, y);
    }

    // Mouse release omitted
}

Gui (Created in NetBeans)

import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import javax.swing.JButton;
import javax.swing.JPanel;

public class DragginTest extends javax.swing.JFrame
{
    public DragginTest()
    {
        initComponents();
        addListeners(jButton1, jButton2, jButton3, jButton4, jButton5, jButton6, jButton7, jButton8, jButton9);
    }

    private void addListeners(JButton... buttons)
    {
        DragListener drag = new DragListener();
        for(JButton b : buttons)
        {
            b.addMouseListener(drag);
            b.addMouseMotionListener(drag); 
        }   
    }

    @SuppressWarnings("unchecked")

    private void initComponents()
    {
        jLayeredPane1 = new javax.swing.JLayeredPane();
        jScrollPane1 = new javax.swing.JScrollPane();
        mainPanel = new javax.swing.JPanel();
        jButton1 = new javax.swing.JButton();
        jButton2 = new javax.swing.JButton();
        jButton3 = new javax.swing.JButton();
        jButton4 = new javax.swing.JButton();
        jButton5 = new javax.swing.JButton();
        jButton6 = new javax.swing.JButton();
        jButton7 = new javax.swing.JButton();
        jButton8 = new javax.swing.JButton();
        jButton9 = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setPreferredSize(new java.awt.Dimension(450, 450));

        mainPanel.setLayout(new java.awt.GridLayout(5, 2, 2, 2));

        // Below Repeated for buttons 1-9 (left out for conciseness)
        jButton1.setFont(new java.awt.Font("Tahoma", 1, 48)); // NOI18N
        jButton1.setForeground(new java.awt.Color(255, 0, 0));
        jButton1.setText("1");
        mainPanel.add(jButton1);
        // End Repeat

        jScrollPane1.setViewportView(mainPanel);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
            .addGap(40, 40, 40)
            .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 205, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addGap(38, 38, 38))
        );
        layout.setVerticalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(layout.createSequentialGroup()
            .addGap(40, 40, 40)
            .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 226, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addContainerGap(53, Short.MAX_VALUE))
        );

        pack();
    }

    public static void main(String args[])
    {
        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                new DragginTest().setVisible(true);
            }
        });
    }

    private javax.swing.JButton jButton1;
    private javax.swing.JButton jButton2;
    private javax.swing.JButton jButton3;
    private javax.swing.JButton jButton4;
    private javax.swing.JButton jButton5;
    private javax.swing.JButton jButton6;
    private javax.swing.JButton jButton7;
    private javax.swing.JButton jButton8;
    private javax.swing.JButton jButton9;
    private javax.swing.JLayeredPane jLayeredPane1;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JPanel mainPanel;
}

回答1:

I added a hack to your DragListener code. Basically it removes the layout manager while you are dragging so the revalidates do nothing and it restores the layout manager when the mouse is released:

import java.awt.*;
import java.awt.Container;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputAdapter;

public class DragListener extends MouseInputAdapter
{
    private Point location;
    private MouseEvent pressed;
    private MouseEvent dragged;
    private MouseEvent dropped;
    private LayoutManager layout;

    @Override
    public void mousePressed(MouseEvent me)
    {
        pressed = me;
        Component component = me.getComponent();
        Container parent = component.getParent();
        parent.setPreferredSize(parent.getPreferredSize());
        layout = parent.getLayout();
        parent.setLayout(null);
    }

    @Override
    public void mouseDragged(MouseEvent me)
    {
        dragged = me;
        Component component = dragged.getComponent();
        Container parent = component.getParent();
        Container superParent = parent.getParent();

        if(superParent instanceof JViewport)
        {
            JViewport vp = (JViewport)superParent;
            Rectangle vpb = vp.getBounds();
            Point pt = MouseInfo.getPointerInfo().getLocation();
            SwingUtilities.convertPointFromScreen(pt, vp);

            if(!vpb.contains(pt))
            {
                int yDiff = (pt.y < vpb.y ) ? pt.y : pt.y - vpb.height;
                vpb.translate(0, yDiff);
                vp.scrollRectToVisible(vpb);
            }
        }

        location = component.getLocation(location);
        int x = location.x - pressed.getX() + me.getX();
        int y = location.y - pressed.getY() + me.getY();
        component.setLocation(x, y);
    }

    // Mouse release omitted
    @Override
    public void mouseReleased(MouseEvent me)
    {
        Component component = me.getComponent();
        Container parent = component.getParent();
        parent.setPreferredSize( null );
        parent.setLayout(layout);
        parent.validate();
        parent.repaint();
    }
}

Of course I assume your real mouseReleased code will have logic to insert the button into the appropriate place in the Container so its real location can be maintained by the GridLayout, otherwise the component will just go back to its original location.

Edit:

Here is a version that moves the button to its new location when the mouse button is released. A little complicated because you need to worry about ZOrder. That is dragging a component down is ok. But if you try to drag a component up, then it gets painted below the other buttons. Temporarily resetting the ZOrder solves this problem.

Boy the code is beginning to be a big hack:) Temporary null layout and temporary ZOrder.

Anyway here is the code:

import java.awt.*;
import java.awt.Container;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import javax.swing.*;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputAdapter;

public class DragListener extends MouseInputAdapter
{
    private Point location;
    private MouseEvent pressed;
    private MouseEvent dragged;
    private MouseEvent dropped;
    private LayoutManager layout;
    private Rectangle originalBounds;
    private int originalZOrder;

    @Override
    public void mousePressed(MouseEvent me)
    {
        pressed = me;
        Component component = me.getComponent();
        Container parent = component.getParent();
        originalBounds = component.getBounds();
        originalZOrder = parent.getComponentZOrder(component);
        parent.setPreferredSize(parent.getPreferredSize());
        layout = parent.getLayout();
        parent.setLayout(null);
        parent.setComponentZOrder(component, 0);
    }

    @Override
    public void mouseDragged(MouseEvent me)
    {
        JComponent source = (JComponent) me.getComponent();
        JComponent parent = (JComponent) source.getParent();

        Point p = me.getPoint();
        p = SwingUtilities.convertPoint(source, p, parent);

        Rectangle bounds = source.getBounds();
        bounds.setLocation(p);

        bounds.x -= pressed.getX();
        bounds.y -= pressed.getY();
        source.setLocation(0, bounds.y);
        parent.scrollRectToVisible(bounds);
    }

    @Override
    public void mouseReleased(MouseEvent me)
    {
        boolean moved = false;
        Component component = me.getComponent();
        Container parent = component.getParent();
        Point location = component.getLocation();

        if (location.y < 0)
        {
            parent.add(component, 0);
            moved = true;
        }
        else
        {
            for (int i = 0; i < parent.getComponentCount(); i++)
            {
                Component c = parent.getComponent(i);
                Rectangle bounds = c.getBounds();

                if (c == component)
                    bounds = originalBounds;

                //  Component is released in the space originally occupied
                //  by the component or over an existing component

                if (bounds.contains(0, location.y))
                {
                    if (c == component)
                    {
                        parent.setComponentZOrder(component, originalZOrder);
                    }
                    else
                    {
                        parent.add(component, i);
                    }

                    moved = true;
                    break;
                }
            }
        }

        //  Component is positioned below all components in the container

        if (!moved)
        {
            parent.add(component, parent.getComponentCount() - 1);
        }

        //  Restore layout manager

        parent.setPreferredSize( null );
        parent.setLayout(layout);
        parent.validate();
        parent.repaint();
        component.requestFocusInWindow();
    }

    private static void createAndShowGUI()
    {
        JPanel panel = new JPanel( new GridLayout(0, 1) );
        DragListener drag = new DragListener();

        for (int i = 0; i <10; i++)
        {
            JButton button = new JButton("" + i);
            button.setFont(new java.awt.Font("Tahoma", 1, 48));
            button.setForeground(new java.awt.Color(255, 0, 0));
            button.addMouseListener(drag);
            button.addMouseMotionListener(drag);
            panel.add( button );
        }

        JFrame frame = new JFrame("SSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add( new JScrollPane(panel) );
        frame.setLocationByPlatform( true );
        frame.setSize(200, 400);
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}


回答2:

You have two core problems, the first is the fact that you are trying to fight the layout manager, which will re-layout the components when it's invalidated, the second is you're going about the drag process in a really, weird way.

When the viewport's viewable area changes, the component is revalidated, which causes the component's position to be re-calculated, causing it to momentarily return to it's initial position.

A choice you have at this moment in time, is to do without it

mainPanel.setLayout(null);
mainPanel.setPreferredSize(new Dimension(200, 600));

// Below Repeated for buttons 1-9 (left out for conciseness)
jButton1.setFont(new java.awt.Font("Tahoma", 1, 48)); // NOI18N
jButton1.setForeground(new java.awt.Color(255, 0, 0));
jButton1.setText("1");
jButton1.setBounds(0, 0, 100, 100);
mainPanel.add(jButton1);

This will cause issues as you introduce more components.

Your mouseDragged method could also be seriously simplified

@Override
public void mouseDragged(MouseEvent me) {
    JComponent source = (JComponent) me.getComponent();
    JComponent parent = (JComponent) source.getParent();

    Point p = me.getPoint();
    p = SwingUtilities.convertPoint(source, p, parent);

    Rectangle bounds = source.getBounds();
    bounds.setLocation(p);  

    bounds.x -= pressed.getX();
    bounds.y -= pressed.getY();
    source.setBounds(bounds);
    parent.scrollRectToVisible(bounds);
}

A better solution would be to make use of the Transferable API and/or Drag'n'Drop API which already exists, basically removing the component from it's current container and re-adding it at a different position within the component hierarchy based on it's drop location. This allows you to continue using layout managers ;)

For an example, have a look at Java - How to drag and drop JPanel with its components

Updated with DnD example

Okay, so the example borrows from Java - How to drag and drop JPanel with its components, but allows you to "reposition" the components when they are dropped. As an added bonus, there is a nice "indicator" of where the component "should" appear...

Because the component is serialized when it's "exported", this causes no end of issues with listeners and the DragGestureRecognizer. To this end, I've implemented a DragDropManager whose sole purpose is to install and uninstall the DragGestureHandler and DragGestureRecognizer at certain points within the drag/drop process...this is why I tend to transfer state and not components :P

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetContext;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class DragginTest extends javax.swing.JFrame {

    public DragginTest() {
        initComponents();
    }

    @SuppressWarnings("unchecked")

    private void initComponents() {
        jScrollPane1 = new javax.swing.JScrollPane();
        mainPanel = new javax.swing.JPanel();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setPreferredSize(new java.awt.Dimension(450, 450));

        mainPanel.setLayout(new GridLayout(10, 0));

        // Below Repeated for buttons 1-9 (left out for conciseness)
        for (int index = 0; index < 10; index++) {
            JButton btn = new JButton(String.valueOf(index));
            btn.setFont(new java.awt.Font("Tahoma", 1, 48)); // NOI18N
            btn.setForeground(new java.awt.Color(255, 0, 0));

            DragDropManager.INSTANCE.installDrag(btn);

            mainPanel.add(btn);
        }
        // End Repeat

        DropHandler dropHandler = new DropHandler();
        DropTarget dropTarget = new DropTarget(mainPanel, DnDConstants.ACTION_MOVE, dropHandler, true);
        mainPanel.setDropTarget(dropTarget);

        jScrollPane1.setViewportView(mainPanel);

        getContentPane().setLayout(new BorderLayout());
        add(jScrollPane1);

        pack();
    }

    public static void main(String args[]) {
        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new DragginTest().setVisible(true);
            }
        });
    }

    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JPanel mainPanel;

    public enum DragDropManager {

        INSTANCE;

        private Map<Component, DragManager> handlers = new HashMap<>(25);

        protected void installDrag(Component comp) {
            handlers.put(comp, new DragManager(comp));
        }

        protected void uninstallDrag(Component comp) {
            DragManager manager = handlers.remove(comp);
            if (manager != null) {
                manager.uninstall();
            }
        }

        protected class DragManager {

            DragGestureHandler dragGestureHandler;
            DragGestureRecognizer dgr;

            public DragManager(Component comp) {
                dragGestureHandler = new DragGestureHandler(comp);
                dgr = DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(
                                comp,
                                DnDConstants.ACTION_MOVE,
                                dragGestureHandler);
            }

            public void uninstall() {
                dgr.removeDragGestureListener(dragGestureHandler);
                dragGestureHandler = null;
                dgr = null;
            }

        }
    }

    public static class ComponentDataFlavor extends DataFlavor {

        // This saves me having to make lots of copies of the same thing
        public static final ComponentDataFlavor SHARED_INSTANCE = new ComponentDataFlavor();

        public ComponentDataFlavor() {

            super(JPanel.class, null);

        }

    }

    public static class ComponentTransferable implements Transferable {

        private DataFlavor[] flavors = new DataFlavor[]{ComponentDataFlavor.SHARED_INSTANCE};
        private Component component;

        public ComponentTransferable(Component panel) {
            this.component = panel;
        }

        @Override
        public DataFlavor[] getTransferDataFlavors() {
            return flavors;
        }

        @Override
        public boolean isDataFlavorSupported(DataFlavor flavor) {

            // Okay, for this example, this is over kill, but makes it easier
            // to add new flavor support by subclassing
            boolean supported = false;

            for (DataFlavor mine : getTransferDataFlavors()) {

                if (mine.equals(flavor)) {

                    supported = true;
                    break;

                }

            }

            return supported;

        }

        public Component getComponent() {

            return component;

        }

        @Override
        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {

            Object data = null;
            if (isDataFlavorSupported(flavor)) {

                data = getComponent();

            } else {

                throw new UnsupportedFlavorException(flavor);

            }

            return data;

        }

    }

    public static class DragGestureHandler implements DragGestureListener, DragSourceListener {

        private Container parent;
        private final Component component;

        public DragGestureHandler(Component child) {

            this.component = child;

        }

        public Component getComponent() {
            return component;
        }

        public void setParent(Container parent) {
            this.parent = parent;
        }

        public Container getParent() {
            return parent;
        }

        @Override
        public void dragGestureRecognized(DragGestureEvent dge) {

            // When the drag begins, we need to grab a reference to the
            // parent container so we can return it if the drop
            // is rejected
            Container parent = getComponent().getParent();

            setParent(parent);

            // Remove the panel from the parent.  If we don't do this, it
            // can cause serialization issues.  We could over come this
            // by allowing the drop target to remove the component, but that's
            // an argument for another day
            parent.remove(getComponent());

            // Update the display
            parent.invalidate();
            parent.repaint();

            // Create our transferable wrapper
            Transferable transferable = new ComponentTransferable(getComponent());

            // Start the "drag" process...
            DragSource ds = dge.getDragSource();
            ds.startDrag(dge, Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR), transferable, this);

            DragDropManager.INSTANCE.uninstallDrag(getComponent());

        }

        @Override
        public void dragEnter(DragSourceDragEvent dsde) {
        }

        @Override
        public void dragOver(DragSourceDragEvent dsde) {
        }

        @Override
        public void dropActionChanged(DragSourceDragEvent dsde) {
        }

        @Override
        public void dragExit(DragSourceEvent dse) {
        }

        @Override
        public void dragDropEnd(DragSourceDropEvent dsde) {

            // If the drop was not sucessful, we need to
            // return the component back to it's previous
            // parent
            if (!dsde.getDropSuccess()) {

                getParent().add(getComponent());

                getParent().invalidate();
                getParent().repaint();

            }
        }
    }

    public class DropHandler implements DropTargetListener {

        private JComponent spacer = new JPanel();

        public DropHandler() {
            spacer.setBackground(Color.RED);
        }

        @Override
        public void dragEnter(DropTargetDragEvent dtde) {

            // Determine if can actual process the contents comming in.
            // You could try and inspect the transferable as well, but 
            // There is an issue on the MacOS under some circumstances
            // where it does not actually bundle the data until you accept the
            // drop.
            if (dtde.isDataFlavorSupported(ComponentDataFlavor.SHARED_INSTANCE)) {

                dtde.acceptDrag(DnDConstants.ACTION_MOVE);

            } else {

                dtde.rejectDrag();

            }

        }

        @Override
        public void dragOver(DropTargetDragEvent dtde) {

            if (dtde.isDataFlavorSupported(ComponentDataFlavor.SHARED_INSTANCE)) {
                Point p = dtde.getLocation();
                DropTargetContext dtc = dtde.getDropTargetContext();
                Container parent = (Container) dtc.getComponent();
                Component target = parent.getComponentAt(p);
                int insertPoint = Math.max(0, parent.getComponentZOrder(target));
                if (spacer.getParent() == null) {
                    parent.add(spacer, insertPoint);
                } else {
                    parent.setComponentZOrder(spacer, insertPoint);
                }
                parent.revalidate();
                parent.repaint();

                Point pic = SwingUtilities.convertPoint(spacer, p, target);
                Rectangle bounds = spacer.getBounds();
                bounds.setLocation(pic);

                ((JComponent) parent).scrollRectToVisible(bounds);
            }
        }

        @Override
        public void dropActionChanged(DropTargetDragEvent dtde) {
        }

        @Override
        public void dragExit(DropTargetEvent dte) {
            Container parent = (Container) dte.getDropTargetContext().getComponent();
            parent.remove(spacer);
            parent.revalidate();
            parent.repaint();
        }

        @Override
        public void drop(DropTargetDropEvent dtde) {

            boolean success = false;

            // Basically, we want to unwrap the present...
            if (dtde.isDataFlavorSupported(ComponentDataFlavor.SHARED_INSTANCE)) {

                Transferable transferable = dtde.getTransferable();
                try {

                    Object data = transferable.getTransferData(ComponentDataFlavor.SHARED_INSTANCE);
                    if (data instanceof Component) {

                        Component target = (Component) data;

                        DropTargetContext dtc = dtde.getDropTargetContext();
                        Component component = dtc.getComponent();

                        if (component instanceof JComponent) {

                            Container parent = target.getParent();
                            if (parent != null) {

                                parent.remove(target);

                            }
                            parent = (Container) component;

                            Point p = dtde.getLocation();
                            Component before = parent.getComponentAt(p);
                            int insertPoint = Math.max(0, parent.getComponentZOrder(before));
                            parent.remove(spacer);
                            System.out.println(insertPoint);
                            parent.add(target, insertPoint);
                            parent.revalidate();
                            parent.repaint();

                            DragDropManager.INSTANCE.installDrag(target);

                            success = true;
                            dtde.acceptDrop(DnDConstants.ACTION_MOVE);

                            invalidate();
                            repaint();

                        } else {

                            success = false;
                            dtde.rejectDrop();

                        }

                    } else {

                        success = false;
                        dtde.rejectDrop();

                    }

                } catch (Exception exp) {

                    success = false;
                    dtde.rejectDrop();
                    exp.printStackTrace();

                }

            } else {

                success = false;
                dtde.rejectDrop();

            }

            dtde.dropComplete(success);

        }

    }
}