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

标签 java swing drag-and-drop jlist autoscroll

我有一个列表,其中包含滚动 Pane 中的许多元素,并且我已经在列表上实现了拖放操作。当我从列表中选择一个项目并将其拖到列表底部时,只要我将鼠标靠近边缘,列表就会自动向下滚动。这在 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 onethis one , 但我不清楚如何实现它们。在我看来,我必须创建一个 DropTarget 子类,并且我使用它的组件应该实现 Autoscroll 接口(interface)。但是 JList 并没有实现它!此外,如果我在列表中设置 DropTarget 而不是 TransferHandler,我是否会丢失 TransferHandler 实现的所有默认拖放行为?

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

最佳答案

bug description 中所述,有两个处理拖放的类:

  • DropTargetAutoScroller, a member class of java.awt.dnd.DropTarget, responsible of supporting components implementing the Autoscroll interface;
  • DropHandler, a member class of javax.swing.TransferHandler, that automates d&d autoscrolling on components implementing the Scrollable interface.

所以,确实,解决方法不适合 JList , 它实现了 Scrollable而不是 Autoscroll .但是,如果您查看 DropTarget 的源代码和 TransferHandler ,您会注意到自动滚动代码基本相同,而且在这两种情况下都是错误的。解决方法也与 DropTarget 非常相似代码,只添加了几行。基本上,解决方案是将鼠标光标的位置从组件坐标系转换到屏幕坐标系。这样,在检查鼠标是否移动时,使用绝对坐标。所以我们可以从TransferHandler复制代码而是添加这几行。

太棒了...但是我们将这段代码放在哪里以及我们如何调用它?

如果我们查看 setTransferHandler()我们看到它实际上设置了一个 DropTarget ,这是一个名为 SwingDropTargetpackage-private static 类来自 TransferHandler类(class)。它将拖放事件委托(delegate)给一个私有(private)静态 DropTargetListener称为 DropHandler .这个类完成了拖放过程中发生的所有魔法,当然它使用了来自 TransferHandler 的其他私有(private)方法。 .这意味着我们不能只设置我们自己的 DropTarget不会丢失 TransferHandler 中已经实现的所有内容.我们可以重写 TransferHandler (大约 1800 行)我们添加了几行来修复错误,但这不是很现实。

一个更简单的解决方案是写一个 DropTargetListener ,其中我们简单地从 DropHandler 复制与自动滚动相关的代码(它也实现了这个接口(interface)),添加了我们的行。这是类:

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() 调用是来自 TransferHandler 代码的额外调用)

接下来,我们可以将此监听器添加到 DropTarget设置 TransferHandler 时安装. (请注意,常规 DropTarget 只接受一个监听器,如果添加另一个监听器将抛出异常。SwingDropTarget 使用 DropHandler ,但幸运的是它也添加了对其他监听器的支持)

所以让我们把这个静态工厂方法添加到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以及可能实现 Scrollable 的任何组件.

关于java - 在 Linux 上拖放错误期间如何实现自动滚动的解决方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29126758/

相关文章:

java - SQLiteException 没有这样的列 : plus (code 1)

java - "pom"类型依赖与范围 "import"和没有 "import"有什么区别?

Java在使用while循环比较最大和最小数字及总和时出现计算错误

java - 抽象 Action lambda

javascript - 脚本式拖动 : How do I offset the dragging element?

html - 拖放多个 div

java - 改变mp3的比特率

java - JTextArea.setText 不可见

java - BorderLayout.CENTER 上 GridBagLayout 面板的垂直对齐

javascript - 在两个不相关的 Web 应用程序之间拖放