代码之家  ›  专栏  ›  技术社区  ›  Andrei Vajna II

如何在Linux上实现拖放错误期间自动滚动的解决方法?

  •  1
  • Andrei Vajna II  · 技术社区  · 10 年前

    我在滚动窗格中有一个包含许多元素的列表,我已经在列表上实现了拖放。当我从列表中选择一个项目并将其拖到列表底部时,只要我将鼠标靠近边缘,列表就会自动向下滚动。这在Windows上运行正常,但在Linux上,列表滚动一个元素,然后停止。

    下面是一个揭示此错误的简短程序:

    import java.awt.BorderLayout;
    import java.awt.Container;
    import java.awt.Dimension;
    import java.awt.datatransfer.DataFlavor;
    import java.awt.datatransfer.Transferable;
    import java.awt.datatransfer.UnsupportedFlavorException;
    import java.io.IOException;
    
    import javax.swing.DropMode;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JList;
    import javax.swing.JPanel;
    import javax.swing.JScrollPane;
    import javax.swing.SwingUtilities;
    import javax.swing.TransferHandler;
    import javax.swing.WindowConstants;
    
    
    public class JListAutoscroll {
    
        protected static Container createUI() {
            JList<String> jlist = new JList<>(generateData(100));
            setDragAndDrop(jlist);
            JScrollPane scrollPane = new JScrollPane(jlist);
            JPanel panel = new JPanel(new BorderLayout());
            panel.add(scrollPane, BorderLayout.CENTER);
            return panel;
        }
    
        private static void setDragAndDrop(JList<String> jlist) {
            jlist.setDragEnabled(true);
            jlist.setDropMode(DropMode.INSERT);
            jlist.setTransferHandler(new ListTransferHandler());
        }
    
        private static String[] generateData(int nRows) {
            String rows[] = new String[nRows];
            for (int i = 0; i < rows.length; i++) {
                rows[i] = "element " + i;
            }
            return rows;
        }
    
        private static class ListTransferHandler extends TransferHandler {
    
            @Override
            public int getSourceActions(JComponent component) {
                return COPY_OR_MOVE;
            }
    
            @Override
            protected Transferable createTransferable(JComponent component) {
                return new ListItemTransferable((JList)component);
            }
    
            @Override
            public boolean canImport(TransferHandler.TransferSupport support) {
                return true;
            }
    
            @Override
            public boolean importData(TransferHandler.TransferSupport support) {
                return true;
            }
        }
    
        private static class ListItemTransferable implements Transferable {
    
            private String item;
    
            public ListItemTransferable(JList<String> jlist) {
                item = jlist.getSelectedValue();
            }
    
            @Override
            public DataFlavor[] getTransferDataFlavors() {
                return new DataFlavor[] { DataFlavor.stringFlavor };
            }
    
            @Override
            public boolean isDataFlavorSupported(DataFlavor flavor) {
                return flavor.equals(DataFlavor.stringFlavor);
            }
    
            @Override
            public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
                if(!isDataFlavorSupported(flavor)) {
                    throw new UnsupportedFlavorException(flavor);
                }
                return item;
            }
    
        }
    
        public static void main(String args[]) {
            SwingUtilities.invokeLater(new Runnable() {
    
                @Override
                public void run() {
                    JFrame frame = new JFrame("JList Autoscroll");
                    frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
                    frame.setContentPane(createUI());
                    frame.setPreferredSize(new Dimension(400, 600));
                    frame.pack();
                    frame.setVisible(true);
                }
    
            });
        }
    
    }
    

    我已经实现了一个简单的TransferHandler,它在拖放时不做任何操作,但足以在拖到列表边缘时显示问题。

    这似乎是JDK中的一个已知错误,在 this report 。我看过一些建议的变通办法,比如 this one this one , 但我不清楚如何实施它们。在我看来,我必须创建一个DropTarget子类,并且与它一起使用的组件应该实现 Autoscroll 界面但是 JList 不执行它!此外,如果我在列表上设置了DropTarget而不是TransferHandler,我不会失去TransferHandler实现的所有默认拖放行为吗?

    那么我如何修改我的程序来解决这个bug呢?

    1 回复  |  直到 10 年前
        1
  •  3
  •   Andrei Vajna II    10 年前

    bug description ,有两个类可以处理拖放:

    • DropTargetAutoScroller ,的成员类 java.awt.dnd.DropTarget ,负责支持实施 Autoscroll 界面
    • DropHandler ,的成员类 javax.swing.TransferHandler ,它自动化了d&d在实现 Scrollable 界面

    因此,确实,解决方法不适合 JList ,实现 可滚动 而不是 自动滚动 。但是,如果您在源代码中查找 DropTarget TransferHandler ,您会注意到自动滚动代码基本相同,并且在两种情况下都是错误的。解决方法也与 删除目标(DropTarget) 代码,只添加了几行。基本上,解决方案是将鼠标光标的位置从组件坐标系转换为屏幕坐标系。这样,当检查鼠标是否移动时,使用绝对坐标。所以我们可以从 传输处理程序 而是添加这几行。

    太好了。。。但是我们把这个代码放在哪里,如何调用它?

    如果我们往里看 setTransferHandler() 我们看到它实际上设置了 删除目标(DropTarget) ,这是一个 包专用静态 类已调用 SwingDropTarget 来自 传输处理程序 班它将拖放事件委派给 专用静态 DropTargetListener 打电话 删除处理程序 。该类执行拖放过程中发生的所有魔术,当然,它还使用来自 传输处理程序 这意味着我们不能自己设定 删除目标(DropTarget) 而不会丢失已在中实现的所有功能 传输处理程序 。我们可以重写 传输处理程序 (大约1800行),我们添加了几行来修复这个bug,但这不是很现实。

    更简单的解决方案是编写 DropTargetListener ,其中我们只需从 删除处理程序 (它也实现了这个接口),并添加了我们的行。这是一个类:

    import java.awt.Insets;
    import java.awt.Point;
    import java.awt.Rectangle;
    import java.awt.Toolkit;
    import java.awt.dnd.DropTargetDragEvent;
    import java.awt.dnd.DropTargetDropEvent;
    import java.awt.dnd.DropTargetEvent;
    import java.awt.dnd.DropTargetListener;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.util.TooManyListenersException;
    
    import javax.swing.JComponent;
    import javax.swing.Scrollable;
    import javax.swing.SwingConstants;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    
    
    public class AutoscrollWorkaround implements DropTargetListener, ActionListener {
    
        private JComponent component;
    
        private Point lastPosition;
    
        private Rectangle outer;
        private Rectangle inner;
    
        private Timer timer;
        private int hysteresis = 10;
    
        private static final int AUTOSCROLL_INSET = 10;
    
        public AutoscrollWorkaround(JComponent component) {
            if (!(component instanceof Scrollable)) {
                throw new IllegalArgumentException("Component must be Scrollable for autoscroll to work!");
            }
            this.component = component;
            outer = new Rectangle();
            inner = new Rectangle();
    
            Toolkit t = Toolkit.getDefaultToolkit();
            Integer prop;
    
            prop = (Integer)t.getDesktopProperty("DnD.Autoscroll.interval");
            timer = new Timer(prop == null ? 100 : prop.intValue(), this);
    
            prop = (Integer)t.getDesktopProperty("DnD.Autoscroll.initialDelay");
            timer.setInitialDelay(prop == null ? 100 : prop.intValue());
    
            prop = (Integer)t.getDesktopProperty("DnD.Autoscroll.cursorHysteresis");
            if (prop != null) {
                hysteresis = prop.intValue();
            }
        }
    
        @Override
        public void dragEnter(DropTargetDragEvent e) {
            lastPosition = e.getLocation();
            SwingUtilities.convertPointToScreen(lastPosition, component);
            updateRegion();
        }
    
        @Override
        public void dragOver(DropTargetDragEvent e) {
            Point p = e.getLocation();
            SwingUtilities.convertPointToScreen(p, component);
    
            if (Math.abs(p.x - lastPosition.x) > hysteresis
                    || Math.abs(p.y - lastPosition.y) > hysteresis) {
                // no autoscroll
                if (timer.isRunning()) timer.stop();
            } else {
                if (!timer.isRunning()) timer.start();
            }
    
            lastPosition = p;
        }
    
        @Override
        public void dragExit(DropTargetEvent dte) {
            cleanup();
        }
    
        @Override
        public void drop(DropTargetDropEvent dtde) {
            cleanup();
        }
    
        @Override
        public void dropActionChanged(DropTargetDragEvent e) {
        }
    
        private void updateRegion() {
            // compute the outer
            Rectangle visible = component.getVisibleRect();
            outer.setBounds(visible.x, visible.y, visible.width, visible.height);
    
            // compute the insets
            Insets i = new Insets(0, 0, 0, 0);
            if (component instanceof Scrollable) {
                int minSize = 2 * AUTOSCROLL_INSET;
    
                if (visible.width >= minSize) {
                    i.left = i.right = AUTOSCROLL_INSET;
                }
    
                if (visible.height >= minSize) {
                    i.top = i.bottom = AUTOSCROLL_INSET;
                }
            }
    
            // set the inner from the insets
            inner.setBounds(visible.x + i.left,
                          visible.y + i.top,
                          visible.width - (i.left + i.right),
                          visible.height - (i.top  + i.bottom));
        }
    
        @Override
        public void actionPerformed(ActionEvent e) {
            updateRegion();
            Point componentPosition = new Point(lastPosition);
            SwingUtilities.convertPointFromScreen(componentPosition, component);
            if (outer.contains(componentPosition) && !inner.contains(componentPosition)) {
                autoscroll(componentPosition);
            }
        }
    
        private void autoscroll(Point position) {
            Scrollable s = (Scrollable) component;
            if (position.y < inner.y) {
                // scroll upward
                int dy = s.getScrollableUnitIncrement(outer, SwingConstants.VERTICAL, -1);
                Rectangle r = new Rectangle(inner.x, outer.y - dy, inner.width, dy);
                component.scrollRectToVisible(r);
            } else if (position.y > (inner.y + inner.height)) {
                // scroll downard
                int dy = s.getScrollableUnitIncrement(outer, SwingConstants.VERTICAL, 1);
                Rectangle r = new Rectangle(inner.x, outer.y + outer.height, inner.width, dy);
                component.scrollRectToVisible(r);
            }
    
            if (position.x < inner.x) {
                // scroll left
                int dx = s.getScrollableUnitIncrement(outer, SwingConstants.HORIZONTAL, -1);
                Rectangle r = new Rectangle(outer.x - dx, inner.y, dx, inner.height);
                component.scrollRectToVisible(r);
            } else if (position.x > (inner.x + inner.width)) {
                // scroll right
                int dx = s.getScrollableUnitIncrement(outer, SwingConstants.HORIZONTAL, 1);
                Rectangle r = new Rectangle(outer.x + outer.width, inner.y, dx, inner.height);
                component.scrollRectToVisible(r);
            }
        }
    
        private void cleanup() {
            timer.stop();
        }
    }
    

    (您会注意到,基本上只有SwingUtilities.convertXYZ()调用是 传输处理程序 代码)

    接下来,我们可以将此侦听器添加到 删除目标(DropTarget) 在设置 传输处理程序 。(注意 删除目标(DropTarget) 只接受一个侦听器,如果添加了另一个,则将抛出异常。 SwingDropTarget(摆动下降目标) 使用 删除处理程序 ,但幸运的是,它还增加了对其他听众的支持)

    所以,让我们将这个静态工厂方法添加到 AutoscrollWorkaround 类,它为我们做到了这一点:

        public static void applyTo(JComponent component) {
            if (component.getTransferHandler() == null) {
                throw new IllegalStateException("A TransferHandler must be set before calling this method!");
            }
            try {
                component.getDropTarget().addDropTargetListener(new AutoscrollWorkaround(component));
            } catch (TooManyListenersException e) {
                throw new IllegalStateException("Something went wrong! DropTarget should have been " +
                        "SwingDropTarget which accepts multiple listeners", e);
            }
        }
    

    这提供了一种简单且非常方便的方法,只需调用这一个方法,即可将解决方法应用于任何遭受此错误的组件。只需确保在 setTransferHandler() 在组件上。因此,我们只需在原始程序中添加一行:

    private static void setDragAndDrop(JList<String> jlist) {
        jlist.setDragEnabled(true);
        jlist.setDropMode(DropMode.INSERT);
        jlist.setTransferHandler(new ListTransferHandler());
        AutoscrollWorkaround.applyTo(jlist); // <--- just this line added
    }
    

    自动滚动现在在Windows和Linux上都可以正常工作。(虽然在Linux上,在自动滚动工作之前,放置位置的行不会重新绘制,但哦。)

    此解决方法也适用于 JTable (我测试过), JTree 以及可能实现的任何组件 可滚动 .