我想实现自动完成功能。目前我有一个包含 JTextField 的 JPanel,当用户开始键入时,会出现一个自动完成(JPopupMenu),其中包含多个选项。
问题是它从文本字段中获取焦点,并且用户不再可以键入。当我将焦点返回到文本字段时,用户不再可以在选项之间导航(使用向上和向下按钮)。 另外,将焦点放在菜单上不允许我拦截其 KeyListener(不知道为什么),当我尝试处理文本字段侧的输入时,我在尝试选择菜单项时遇到问题。
所以我想要:
- 带有选项的弹出菜单,当用户更改文本字段中的文本时,该菜单会动态更改,但菜单仍然处于 Activity 状态
- 用户可以使用向上和向下箭头键以及 Enter 和 Escape 键在选项之间导航以分别使用选项或关闭弹出窗口。
是否可以处理菜单上的键盘事件并将键入事件转发回文本字段?
解决我的问题的正确方法是什么?
这是下面的代码。提前致谢!
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
class TagVisual extends JPanel {
private JTextField editField;
public TagVisual() {
FlowLayout layout = new FlowLayout();
layout.setHgap(0);
layout.setVgap(0);
setLayout(layout);
editField = new JTextField();
editField.setBackground(Color.RED);
editField.setPreferredSize(new Dimension(200, 20));
editField.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
JPopupMenu menu = new JPopupMenu();
menu.add("Item 1");
menu.add("Item 2");
menu.add("Item 3");
menu.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
JOptionPane.showMessageDialog(TagVisual.this, "keyTyped");
}
@Override
public void keyPressed(KeyEvent e) {
JOptionPane.showMessageDialog(TagVisual.this, "keyPressed");
}
@Override
public void keyReleased(KeyEvent e) {
JOptionPane.showMessageDialog(TagVisual.this, "keyReleased");
}
});
menu.show(editField, 0, getHeight());
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
}
});
add(editField, FlowLayout.LEFT);
}
public void place(JPanel panel) {
panel.add(this);
editField.grabFocus();
}
}
public class MainWindow {
private JPanel mainPanel;
private JFrame frame;
public MainWindow(JFrame frame) {
mainPanel = new JPanel(new FlowLayout());
TagVisual v = new TagVisual();
v.place(mainPanel);
this.frame = frame;
}
public static void main(String[] args) {
JFrame frame = new JFrame("TextFieldPopupIssue");
frame.setContentPane(new MainWindow(frame).mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
}
最佳答案
我个人建议使用弹出窗口或自定义的 JWindow
而不是 JPopupMenu
,因为后者最初仅用于显示菜单项。它确实适用于其他事情,但以不同方式使用它并不是最佳实践。
例如,您的示例中有一些菜单项作为自动完成选项 - 如果只有几个结果,则效果很好。但如果有 10 个呢?如果50个呢?还是500?您将不得不以某种方式为这些情况创建额外的解决方法 - 要么将项目放入滚动 Pane (天哪,这看起来很难看),要么将结果减少到一些最佳结果(这也不是最好的选择)。
因此,我使用 JWindow
作为 AutocompleteField
的弹出窗口制作了一个小示例。它非常简单,但是做了一些您期望的基本事情以及您提到的事情:
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author Mikle Garin
* @see https://stackoverflow.com/questions/45439231/implementing-autocomplete-with-jtextfield-and-jpopupmenu
*/
public final class AutocompleteField extends JTextField implements FocusListener, DocumentListener, KeyListener
{
/**
* {@link Function} for text lookup.
* It simply returns {@link List} of {@link String} for the text we are looking results for.
*/
private final Function<String, List<String>> lookup;
/**
* {@link List} of lookup results.
* It is cached to optimize performance for more complex lookups.
*/
private final List<String> results;
/**
* {@link JWindow} used to display offered options.
*/
private final JWindow popup;
/**
* Lookup results {@link JList}.
*/
private final JList list;
/**
* {@link #list} model.
*/
private final ListModel model;
/**
* Constructs {@link AutocompleteField}.
*
* @param lookup {@link Function} for text lookup
*/
public AutocompleteField ( final Function<String, List<String>> lookup )
{
super ();
this.lookup = lookup;
this.results = new ArrayList<> ();
final Window parent = SwingUtilities.getWindowAncestor ( this );
popup = new JWindow ( parent );
popup.setType ( Window.Type.POPUP );
popup.setFocusableWindowState ( false );
popup.setAlwaysOnTop ( true );
model = new ListModel ();
list = new JList ( model );
popup.add ( new JScrollPane ( list )
{
@Override
public Dimension getPreferredSize ()
{
final Dimension ps = super.getPreferredSize ();
ps.width = AutocompleteField.this.getWidth ();
return ps;
}
} );
addFocusListener ( this );
getDocument ().addDocumentListener ( this );
addKeyListener ( this );
}
/**
* Displays autocomplete popup at the correct location.
*/
private void showAutocompletePopup ()
{
final Point los = AutocompleteField.this.getLocationOnScreen ();
popup.setLocation ( los.x, los.y + getHeight () );
popup.setVisible ( true );
}
/**
* Closes autocomplete popup.
*/
private void hideAutocompletePopup ()
{
popup.setVisible ( false );
}
@Override
public void focusGained ( final FocusEvent e )
{
SwingUtilities.invokeLater ( () -> {
if ( results.size () > 0 )
{
showAutocompletePopup ();
}
} );
}
private void documentChanged ()
{
SwingUtilities.invokeLater ( () -> {
// Updating results list
results.clear ();
results.addAll ( lookup.apply ( getText () ) );
// Updating list view
model.updateView ();
list.setVisibleRowCount ( Math.min ( results.size (), 10 ) );
// Selecting first result
if ( results.size () > 0 )
{
list.setSelectedIndex ( 0 );
}
// Ensure autocomplete popup has correct size
popup.pack ();
// Display or hide popup depending on the results
if ( results.size () > 0 )
{
showAutocompletePopup ();
}
else
{
hideAutocompletePopup ();
}
} );
}
@Override
public void focusLost ( final FocusEvent e )
{
SwingUtilities.invokeLater ( this::hideAutocompletePopup );
}
@Override
public void keyPressed ( final KeyEvent e )
{
if ( e.getKeyCode () == KeyEvent.VK_UP )
{
final int index = list.getSelectedIndex ();
if ( index != -1 && index > 0 )
{
list.setSelectedIndex ( index - 1 );
}
}
else if ( e.getKeyCode () == KeyEvent.VK_DOWN )
{
final int index = list.getSelectedIndex ();
if ( index != -1 && list.getModel ().getSize () > index + 1 )
{
list.setSelectedIndex ( index + 1 );
}
}
else if ( e.getKeyCode () == KeyEvent.VK_ENTER )
{
final String text = ( String ) list.getSelectedValue ();
setText ( text );
setCaretPosition ( text.length () );
}
else if ( e.getKeyCode () == KeyEvent.VK_ESCAPE )
{
hideAutocompletePopup ();
}
}
@Override
public void insertUpdate ( final DocumentEvent e )
{
documentChanged ();
}
@Override
public void removeUpdate ( final DocumentEvent e )
{
documentChanged ();
}
@Override
public void changedUpdate ( final DocumentEvent e )
{
documentChanged ();
}
@Override
public void keyTyped ( final KeyEvent e )
{
// Do nothing
}
@Override
public void keyReleased ( final KeyEvent e )
{
// Do nothing
}
/**
* Custom list model providing data and bridging view update call.
*/
private class ListModel extends AbstractListModel
{
@Override
public int getSize ()
{
return results.size ();
}
@Override
public Object getElementAt ( final int index )
{
return results.get ( index );
}
/**
* Properly updates list view.
*/
public void updateView ()
{
super.fireContentsChanged ( AutocompleteField.this, 0, getSize () );
}
}
/**
* Sample {@link AutocompleteField} usage.
*
* @param args run arguments
*/
public static void main ( final String[] args )
{
final JFrame frame = new JFrame ( "Sample autocomplete field" );
// Sample data list
final List<String> values = Arrays.asList ( "Frame", "Dialog", "Label", "Tree", "Table", "List", "Field" );
// Simple lookup based on our data list
final Function<String, List<String>> lookup = text -> values.stream ()
.filter ( v -> !text.isEmpty () && v.toLowerCase ().contains ( text.toLowerCase () ) && !v.equals ( text ) )
.collect ( Collectors.toList () );
// Autocomplete field itself
final AutocompleteField field = new AutocompleteField ( lookup );
field.setColumns ( 15 );
final JPanel border = new JPanel ( new BorderLayout () );
border.setBorder ( new EmptyBorder ( 50, 50, 50, 50 ) );
border.add ( field );
frame.add ( border );
frame.setDefaultCloseOperation ( WindowConstants.EXIT_ON_CLOSE );
frame.pack ();
frame.setLocationRelativeTo ( null );
frame.setVisible ( true );
}
}
因此,在此示例中,弹出窗口 JWindow
本身不处于 Activity 状态(未聚焦),并且无法获得焦点,因为它被强制配置为如此。这使我们能够将焦点保持在 JTextField
内并继续输入。
在此示例中,我们还捕获字段中的向上/向下箭头等关键事件,以在自动完成结果中导航。 ENTER和ESCAPE用于接受/取消结果选择。
此代码也可以稍微重写以使用 Swing PopupFactory
作为自动完成弹出窗口的源,但本质上仍然是相同的,因为使用了 HeavyWeightWindow
by PopupFactory
只是扩展了 JWindow
并添加了一些设置。
关于java - 使用 jtextfield 和 jpopupmenu 实现自动完成,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45439231/