输入文本时覆盖 Swing 文本标签

Overlay Swing Text Label as Text is Entered

我程序中的各种表单使用 JTables,我已经能够使用 Key Listener 在用户键入时 select 一行。这很好用,但我想向用户提供一些反馈以显示正在输入的文本。

我尝试创建 frame/labels 但无法正确显示它们。

我的基本想法是 - 创建框架(如果尚不存在),创建标签并设置文本。例如:

private void showSearchLabel(String search) {
    if (null == searchTextFrame) {
        searchTextFrame = new JFrame("searchTextFrame");
        searchTextFrame.setBackground(new Color(0, 0, 0, 0));
        searchTextFrame.setUndecorated(true);
        searchTextFrame.setAlwaysOnTop(true);

        searchTextFrame.getContentPane().setLayout(new java.awt.BorderLayout());
        searchTextLabel = new JLabel();
        searchTextFrame.getContentPane().add(searchTextLabel);
        searchTextFrame.pack();
        searchTextFrame.setVisible(true);
    }

    searchTextLabel.setText(search);

}

showSearchLabel 由 Key Listener 调用,它将最近的按键添加到搜索字符串中。退格键清除字符串(并删除 frame/label)。输入键 selects table 中的项目并且还应删除 frame/label。

我错过了什么?

编辑: 澄清 - 使用上面的代码,什么也没有显示。

如果我在创建标签时设置了文本,则第一个字符是可见的(这是预期的,因为此时用户只输入了一个字符)。在此之后调用 .setText(search),文本不会更新。注意 - 这在屏幕的 top/left 手角可见,这并不是我真正想要的位置(理想情况下,希望它显示在 JTable 中)。

一个新的 JLabel() "an empty string for the title." 它的首选大小为零。你可以

  • 在构造函数中添加需要的space。

    searchTextLabel = new JLabel("        ");
    
  • 在调用 setVisible().

    之后,在封闭框架上调用 pack()
    f.setVisible(true);
    …
    searchTextFrame.pack();
    
  • 在构造函数中添加一个 space 来建立高度,并在封闭的 Container.

    上调用 validate()
    searchTextLabel = new JLabel(" ");
    …
    f.setVisible(true);
    …
    searchTextLabel.setText(search);
    searchTextLabel.validate();
    

在过去的几个月里,我不得不多次重复这样的事情,使用了多种不同的 UI 方法(隐藏一个字段直到用户输入,然后通过BorderLayout;屏幕上的静态字段;等等...),并试图构建一个更简洁和可重用的库机制,这将使我能够配置任何 JTable 并提供简单的可过滤支持。

我在 JLabel 上使用 JTextField 的原因之一是它比你或我可能(在合理的时间内)能够产生的任何东西更好地处理用户输入,但这就是我 ;)

就个人而言,我不喜欢打开任何新的 windows,因为这会引入其他问题,例如与焦点相关的问题以及 window 隐藏在当前 window 之后在某些平台等

同样,有几次我 "unhidden" 一个字段,布局被迫改变,这看起来很难看,而且并不总是需要让过滤器字段一直可见。相反,我试图实现的是一个 "popup" 字段,它与 table 本身内联,但能够在父 JScrollPane 的可视区域内的任何地方可见。 ..

现在,这种方法可能不适合您的需求,但这是我一直努力的方向...

同样,核心概念是提供不需要我扩展 JTable 但可以环绕现有 JTable 的功能。核心功能由一个实用程序 class 提供,并通过两个 interfaces 进行管理,这两个 interfaces 提供对某些核心功能的外部控制(过滤器如何应用于 JTable 以及如何取消操作有效)。该实用程序 class 还提供了配置取消过滤字段的键盘操作的方法

显示的 JTextField 实际上是添加到 JTable 本身。我曾考虑过尝试将 "offset" 强制放入视图中,因此所有行都被向下推,但我知道如何做到这一点的唯一方法是扩展 JTable 本身,这就是我试图避免

public class TableUtilities {

    private static WeakHashMap<JTable, FilterSupport> mapFilters = new WeakHashMap(25);

    public static void installFilterSupport(JTable table, IFilterListener listener, KeyStroke escapeKey) {
        FilterSupport support = new FilterSupport(table, listener, escapeKey);
        mapFilters.put(table, support);
    }

    public static void uninstallFilterSupport(JTable table) {
        FilterSupport support = mapFilters.remove(table);
        if (support != null) {
            support.uninstall();
        }
    }

    protected static class FilterSupport implements IFilterSupport {

        private JViewport viewport;
        private JTable table;
        private JTextField searchField;
        private Timer filterTimer;

        private HierarchyListener hierarchyListener;
        private ChangeListener changeListener;
        private IFilterListener filterListener;

        public FilterSupport(JTable table, IFilterListener listener, KeyStroke escapeKey) {
            this.table = table;
            this.filterListener = listener;
            table.setFillsViewportHeight(true);

            hierarchyListener = new HierarchyListener() {
                @Override
                public void hierarchyChanged(HierarchyEvent e) {
                    long flags = e.getChangeFlags();
                    if ((flags & HierarchyEvent.PARENT_CHANGED) != 0) {
                        if (e.getChanged().equals(table)) {
                            JTable table = (JTable) e.getChanged();
                            if (e.getChangedParent() instanceof JViewport) {
                                if (table.getParent() == null) {
                                    uninstall();
                                } else {
                                    install();
                                }
                            }
                        }
                    }
                }
            };
            changeListener = new ChangeListener() {
                @Override
                public void stateChanged(ChangeEvent e) {
                    JViewport viewport = (JViewport) e.getSource();
                    Rectangle viewRect = viewport.getViewRect();
                    searchField.setSize(searchField.getPreferredSize());
                    int x = (viewRect.x + viewRect.width) - searchField.getWidth();
                    int y = viewRect.y;
                    searchField.setLocation(x, y);
                }
            };

            table.addHierarchyListener(hierarchyListener);

            searchField = new JTextField(20);
            searchField.setVisible(false);
            searchField.getDocument().addDocumentListener(new DocumentListener() {

                @Override
                public void insertUpdate(DocumentEvent e) {
                    filterChanged();
                }

                @Override
                public void removeUpdate(DocumentEvent e) {
                    filterChanged();
                }

                @Override
                public void changedUpdate(DocumentEvent e) {
                    filterChanged();
                }
            });

            searchField.addFocusListener(new FocusAdapter() {
                @Override
                public void focusLost(FocusEvent e) {
                    cancelField();
                }
            });

            filterTimer = new Timer(250, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    applyFilter();
                }
            });
            filterTimer.setRepeats(false);

            table.addKeyListener(new KeyAdapter() {

                @Override
                public void keyTyped(KeyEvent e) {
                    if (Character.isLetterOrDigit(e.getKeyChar())) {
                        searchField.setVisible(true);
                        table.revalidate();
                        table.repaint();
                        // ?? Should this maintain the current filter value?
                        searchField.setText(null);
                        searchField.requestFocusInWindow();
                        SwingUtilities.invokeLater(new Runnable() {
                            @Override
                            public void run() {
                                searchField.dispatchEvent(e);
                            }
                        });

                    }
                }

            });

            Action escapeAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    cancelField();
                }
            };

            bindKeyStrokeTo(table, JComponent.WHEN_FOCUSED, "clear", escapeKey, escapeAction);
            bindKeyStrokeTo(searchField, JComponent.WHEN_FOCUSED, "clear", escapeKey, escapeAction);

        }

        protected void cancelField() {
            searchField.setVisible(false);
            table.requestFocusInWindow();
            table.revalidate();
            table.repaint();
            if (filterListener != null) {
                filterListener.filterCancelled(table, this);
            }
        }

        public void filterChanged() {
            filterTimer.restart();
        }

        protected void applyFilter() {

            if (filterListener != null) {
                filterListener.filterChanged(table, searchField.getText());
            }

        }

        protected void uninstall() {
            filterTimer.stop();
            if (viewport != null) {
                if (changeListener != null) {
                    viewport.removeChangeListener(changeListener);
                }
                table.remove(searchField);
                searchField.setVisible(false);
            }
            viewport = null;
        }

        protected void install() {
            if (viewport != null) {
                uninstall();
            }
            Container parent = table.getParent();
            if (parent instanceof JViewport) {
                viewport = (JViewport) parent;
                viewport.addChangeListener(changeListener);
                table.add(searchField);
            }
        }

        @Override
        public String getFilter() {
            return searchField.getText();
        }

        @Override
        public void setFilter(String filter) {
            searchField.setText(filter);
        }

    }

    public static void bindKeyStrokeTo(JComponent parent, int condition, String name, KeyStroke keyStroke, Action action) {
        InputMap im = parent.getInputMap(condition);
        ActionMap am = parent.getActionMap();

        im.put(keyStroke, name);
        am.put(name, action);
    }

    public static interface IFilterSupport {

        public String getFilter();

        public void setFilter(String filter);

    }

    public static interface IFilterListener {

        public void filterChanged(JTable table, String filter);

        public void filterCancelled(JTable table, IFilterSupport support);
    }

}

还有我的测试class...

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.WeakHashMap;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.RowFilter;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableRowSorter;

public class TestSearchTable {

    public static void main(String[] args) {
        new TestSearchTable();
    }

    public TestSearchTable() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new BorderLayout());

            DefaultTableModel model = new DefaultTableModel(
                            new Object[][]{
                                {"Tiana", "Wilmer"},
                                {"Twana", "Wingate"},
                                {"Cody", "Baumgarten"},
                                {"Venus", "Espy"},
                                {"Savanna", "Buckmaster"},
                                {"Adrien", "Edgecomb"},
                                {"Lauretta", "Sassman"},
                                {"Vivienne", "Glasco"},
                                {"Cassy", "Merryman"},
                                {"Mitchel", "Jarvie"},
                                {"Kelsi", "Casebeer"},
                                {"Rosy", "Rizzi"},
                                {"Bernice", "Capote"},
                                {"Tijuana", "Launius"},
                                {"Jeffie", "Crownover"},
                                {"Selena", "Leavy"},
                                {"Damon", "Tulloch"},
                                {"Norris", "Devitt"},
                                {"Cecil", "Burgio"},
                                {"Queen", "Mechling"}},
                            new Object[]{"First Name", "Last name"}
            ) {

                @Override
                public boolean isCellEditable(int row, int column) {
                    return false;
                }

            };

            JTable table = new JTable(model);
            table.setAutoCreateRowSorter(true);
            TableUtilities.installFilterSupport(table,
                            new TableUtilities.IFilterListener() {
                                @Override
                                public void filterChanged(JTable table, String filter) {
                                    TableRowSorter sorter = (TableRowSorter) table.getRowSorter();
                                    if (filter == null || filter.trim().length() == 0) {
                                        filter = "*";
                                    }

                                    if (!filter.startsWith("*") || !filter.endsWith("*")) {
                                        filter = "*" + filter + "*";
                                    }
                                    filter = wildcardToRegex(filter);
                                    filter = "(?i)" + filter;
                                    sorter.setRowFilter(RowFilter.regexFilter(filter));
                                }

                                @Override
                                public void filterCancelled(JTable table, TableUtilities.IFilterSupport support) {
//                                  support.setFilter(null);
                                }
                            },
                            KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
            add(new JScrollPane(table));
        }

    }

    public static String wildcardToRegex(String wildcard) {

        StringBuilder s = new StringBuilder(wildcard.length());
        s.append('^');

        for (int i = 0, is = wildcard.length(); i < is; i++) {

            char c = wildcard.charAt(i);
            switch (c) {
                case '*':
                    s.append(".*");
                    break;
                case '?':
                    s.append(".");
                    break;
                // escape special regexp-characters
                case '(':
                case ')':
                case '[':
                case ']':
                case '$':
                case '^':
                case '.':
                case '{':
                case '}':
                case '|':
                case '\':
                    s.append("\");
                    s.append(c);
                    break;
                default:
                    s.append(c);
                    break;
            }

        }

        s.append('$');
        return (s.toString());

    }

}