简体   繁体   中英

JPanel in JComboBox dropdown but not in the editor

What I want seems relatively simple, and I'm almost there. This will eventually be used to extend TableCellEditor , so the size is important. What I want is something like this: 模拟理想的JPanelComboBox

With a combination of custom ComboBoxEditor s and ListCellRenderer s I've been able to get something like this:

模拟JPanelComboBox的最佳尝试

Which has the inconveniences of:

  • Cutting off any components beyond the original width of the JComboBox
  • Forcing the height of the JComboBox to be the height of the JPanel in the drop-down
  • Allowing only one (1) click modification of the form before the drop-down disappears.

I'd like to have the drop-down stay visible until the user clicks the editor or the JComboBox looses focus to another control, and then have the value in the editor update. There will only ever be one (1) JPanel in the drop-down and I don't want the editor to be able to actually edit the string displayed.

My question is similar to @ErkanHaspalut 's question here but neither response is satisfying. I'd previously made a similar attempt by embedding a JPanel in a JPopupMenu and adding it to a JTextField but had similar issues about the popup disappearing prematurely.

I've tried forcing the size of the JComboBox both by setting the setMaximumSize height value (which has no effect) and with

    Rectangle tmp = cboTest.getBounds();
    tmp.height = 24;
    cboTest.setBounds(tmp);

which simply shows the top 24 lines of the JComboBox . A minimum compilable example would be

/*
    * Program to test having a JPanel in the JComboBox's drop-down but not the JComboBox's editor.
    */
    package testdropdownsubform;

    import java.awt.Component;
    import java.awt.Rectangle;
    import java.awt.event.ActionListener;
    import java.awt.event.FocusEvent;
    import java.awt.event.FocusListener;
    import javafx.application.Application;
    import javafx.stage.Stage;
    import javax.swing.ComboBoxEditor;
    import javax.swing.JComboBox;
    import javax.swing.JList;
    import javax.swing.ListCellRenderer;
    import javax.swing.event.PopupMenuEvent;
    import javax.swing.event.PopupMenuListener;
    import javax.swing.JTextField;
    import javax.swing.plaf.basic.BasicComboBoxEditor;

    /**
    * @author Masked Coder
    */

    public class Dim {
        public Long DimWidth;
        public Long DimHeight;

        public Dim () {
            DimWidth = 1L;
            DimHeight = 1L;
        }

        @Override
        public String toString() {
            return DimWidth.toString() + "\' x " + DimHeight.toString() + "\'";
        }
    }

    public class DimPanel extends javax.swing.JPanel {

        public DimPanel() {
            spnDimWidth = new javax.swing.JSpinner();
            spnDimHeight = new javax.swing.JSpinner();

            spnDimWidth.setModel(new javax.swing.SpinnerNumberModel(Long.valueOf(1L), Long.valueOf(0L), null, Long.valueOf(1L)));
            spnDimWidth.setPreferredSize(new java.awt.Dimension(50, 24));
            addComponent(spnDimWidth);

            lblTween.setText(" x ");
            addComponent(lblTween);

            spnDimHeight.setModel(new javax.swing.SpinnerNumberModel(Long.valueOf(1L), Long.valueOf(0L), null, Long.valueOf(1L)));
            spnDimHeight.setPreferredSize(new java.awt.Dimension(50, 24));
            addComponent(spnDimHeight);
        }

        private javax.swing.JSpinner spnDimWidth;
        private javax.swing.JLabel lblTween;
        private javax.swing.JSpinner spnDimHeight;
    }


    public class DimListCellRenderer implements ListCellRenderer {
        private DimPanel dpDim;

        public DimListCellRenderer(DimPanel newDimPanel) {
            dpDim = newDimPanel;

        }

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            Dim dValue = (Dim) value;
            dpDim.setDim(dValue);
            return dpDim;
        }

    }

    public class DimComboBoxEditor extends BasicComboBoxEditor implements ComboBoxEditor {
        JTextField txtDim = new JTextField();
        Dim Item = new Dim();

        public DimComboBoxEditor() {
            txtDim.setEnabled(false);
            txtDim.setOpaque(true);
        }
        @Override
        public Component getEditorComponent() {
            txtDim.setText(Item.toString());
            return txtDim;
        }

        @Override
        public void setItem(Object anObject) {
            Item = (Dim) anObject;
        }

        @Override
        public Object getItem() {
            return Item;
        }

        @Override
        public void selectAll() {
            txtDim.selectAll();
        }

        @Override
        public void addActionListener(ActionListener l) {
            txtDim.addActionListener(l);
        }

        @Override
        public void removeActionListener(ActionListener l) {
            txtDim.removeActionListener(l);
        }

    }

    public class MainTestForm extends javax.swing.JFrame {
        public MainTestForm() {
            lblPrevComponent = new javax.swing.JLabel();
            chkPrevComponent = new javax.swing.JCheckBox();
            lblTest = new javax.swing.JLabel();
            cboTest = new JComboBox<testdropdownsubform.DicePanel>();
            lblNextComponent = new javax.swing.JLabel();
            scpNextComponent = new javax.swing.JScrollPane();
            txaNextComponent = new javax.swing.JTextArea();
            btnForceHeight = new javax.swing.JButton();

            setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

            lblPrevComponent.setText("Prev. Component");

            chkPrevComponent.setText("jCheckBox1");

            lblTest.setText("Dimension");

            cboTest.setEditable(true);
            cboTest.setEditor(new DimComboBoxEditor());
            cboTest.setRenderer(new DimListCellRenderer(new DimPanel()));
            cboTest.addItem(new Dim());

            lblNextComponent.setText("Next Component");

            txaNextComponent.setColumns(20);
            txaNextComponent.setRows(5);
            scpNextComponent.setViewportView(txaNextComponent);

            btnForceHeight.setText("Force");
            btnForceHeight.setToolTipText("Force test combobox height");
            btnForceHeight.addActionListener(new java.awt.event.ActionListener() {
                public void actionPerformed(java.awt.event.ActionEvent evt) {
                    btnForceHeightActionPerformed(evt);
                }
            });

            .addComponent(lblPrevComponent)
            .addComponent(chkPrevComponent))
            .addComponent(lblTest)
            .addComponent(cboTest)
            .addComponent(lblNextComponent)
            .addComponent(scpNextComponent)
            .addComponent(btnForceHeight))

        }

    private void btnForceHeightActionPerformed(java.awt.event.ActionEvent evt) {                                               
            Rectangle tmp = cboTest.getBounds();
            tmp.height = 24;
            cboTest.setBounds(tmp);
    }                                              

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

        private javax.swing.JButton btnForceHeight;
        private javax.swing.JComboBox cboTest;
        private javax.swing.JCheckBox chkPrevComponent;
        private javax.swing.JLabel lblNextComponent;
        private javax.swing.JLabel lblPrevComponent;
        private javax.swing.JLabel lblTest;
        private javax.swing.JScrollPane scpNextComponent;
        private javax.swing.JTextArea txaNextComponent;
    }

    public class TestDropdownSubform extends Application {

        @Override
        public void start(Stage primaryStage) {
            MainTestForm mtfMain = new MainTestForm();

            mtfMain.setVisible(true);
        }

        /**
        * @param args the command line arguments
        */
        public static void main(String[] args) {
            launch(args);
        }

    }

My question is, am I missing a detail, or can anyone see a better way to accomplish this? Thanks in advance for your advice.

Edit : Declaring the JComboBox as

JComboBox cboTest = new JComboBox<DimPanel>() {
    private boolean layingOut = false; 

    @Override
    public void doLayout(){ 
        try{ 
        layingOut = true; 
        super.doLayout(); 
        }finally{ 
        layingOut = false; 
        } 
    } 

        @Override
        public Dimension getSize(){ 
            Dimension dim = super.getSize(); 
            if(!layingOut) {
                dim.width = Math.max(dim.width, dpThis.getPreferredSize().width); 
            }
            return dim; 
        } 
};

fixes the width of the drop-down. Declaring it as

JComboBox cboTest = new JComboBox<testdropdownsubform.DicePanel>() {
    private boolean layingOut = false; 

    @Override
    public void doLayout(){ 
        try{ 
        layingOut = true; 
        super.doLayout(); 
        }finally{ 
        layingOut = false; 
        } 
    } 

        @Override
        public Dimension getSize(){ 
            Dimension dim = super.getSize(); 
            if(!layingOut) {
                Dimension dim2 = dpThis.getPreferredSize(); 
                dim.width = Math.max(dim.width, dim2.width); 
    //          dim.height = dim2.height; 
            }
            return dim; 
        } 

    @Override
    public DimPanel getPrototypeDisplayValue() {
        DimPanel tmpPanel = new DimPanel();
        if(isCalledFromComboPopup()) {
        //
        }
        else {
        Dimension r = dcbeEditor.getPreferredSize();
        tmpPanel.setPreferredSize(r);
        }
        return tmpPanel;
    }

    /**
    * Hack method to determine if called from within the combo popup UI.
    */
    public boolean isCalledFromComboPopup() {
        try {
        final Throwable t = new Throwable();
        t.fillInStackTrace();
        StackTraceElement[] st = t.getStackTrace();
        // look only at top 5 elements of call stack
        int max = Math.min(st.length, 5);
        for (int i=0; i<max; ++i) {
            final String name = st[i].getClassName();
            System.out.println(i + ")  " + name);
            return ((name != null) && name.contains("ComboPopup"));
        }
        } catch (final Exception e) {
        // if there was a problem, assume not called from combo popup
        }
        return false;
    }
};

fixes the editor size but now the drop-down is the width and height of the editor.

So, I ended up going on a different track, creating my own pseudo-combobox with a JTextField and a JButton in a JPanel . I believe I've quite handily genericized the result into four related classes. First, you need an implementation of AbstractPopupPanel (which quite handily loads into the NetBeans designer if you're using it):

package complexcombobox;

/**
*
* @author MaskedCoder
*
* @param E - the object to be edited in the panel
*/
public abstract class AbstractPopupPanel<E> extends javax.swing.JPanel {

    /**
    * Creates new form PopupPanel
    */
    public AbstractPopupPanel() {
        initComponents();
    }

    public abstract boolean isChanged();

    public abstract E getValue();

    public abstract void setValue(E newValue);

    private void initComponents() {
        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 400, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 300, Short.MAX_VALUE)
        );
    }                        
}

With that created to your satisfaction, we need to place it in a pop-up menu:

package complexcombobox;

import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

/**
*
* @author MaskedCoder
* 
* @param E - the object to be edited
* @param P - the extension of AbstractPopupPanel that will display the object as you wish
*/
public class ComplexPopup<E, P extends AbstractPopupPanel<E>> extends JPopupMenu {

    /*
    * Interface to notify user of editing
    */
    public interface EditListener<E> {
        // return the object's value to be edited.
        // if the user returns null, the existing Dice is used.
        public E beforeEdit(); 

        // receives the new value
        // called *only* when the object has actually changed.
        public void afterEdit(E newValue);  
    }

    /*
    *internal variables
    */
    private P thisPanel;
    private EditListener<E> userListener;
//  private Class<E> eClass;
//  private Class<P> pClass;
    private PopupMenuListener myPopupListener = new PopupMenuListener() {

        @Override
        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            if(userListener != null) {
                E tmpSize = userListener.beforeEdit();
                if(tmpSize != null) thisPanel.setValue(tmpSize);
            }
        }

        @Override
        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            if(userListener != null) {
//              if(thisPanel.isChanged())
                    userListener.afterEdit((E) thisPanel.getValue());
            }
        }

        @Override
        public void popupMenuCanceled(PopupMenuEvent e) {
            popupMenuWillBecomeInvisible(e);
        }
    };

    /* 
    * Constructors
    */

    public ComplexPopup(E iniValue, P iniDropdownPanel, EditListener<E> iniListener) {
        super();

        init(iniValue, iniDropdownPanel, iniListener);
    }

    private void init(E iniValue, P iniDropdownPanel, EditListener<E> iniListener) {
        thisPanel = iniDropdownPanel;
        thisPanel.setValue(iniValue);
        add(thisPanel);
        userListener = iniListener;
        this.addPopupMenuListener(myPopupListener);
    }

    /* 
    * Public Properties 
    */
    public E getValue() {
        return (E) thisPanel.getValue();
    }
    public void setValue(E newObjectSize) {
        thisPanel.setValue(newObjectSize);
    }

    public EditListener getUserListener() {
        return userListener;
    }

    public void setEditListener(EditListener newListener) {
        userListener = newListener;
    }

    /*
    * functional extensions
    */
    public void afterEdit(E newValue) {
        if(userListener != null) {
            if(thisPanel.isChanged())
                userListener.afterEdit((E) thisPanel.getValue());
        }
    }
}

This places the form in a JPopupMenu and allows access to the object to be edited as well as notification before and after editing. Now, I couldn't co-opt the JComboBox interface without getting deeper into the mechanics than I wanted to go, so I fudged the combo box, as I mentioned:

package complexcombobox;

/**
*
* @author MaskedCoder
* 
* @param <I> the Item (Object) to be edited
* @param <Q> the AbstractPopupPanel to be used to do the editing. 
*/
public class ComplexComboBox<I, Q extends AbstractPopupPanel<I>> extends javax.swing.JPanel {
    private ComplexPopup<I, Q> thePopup;
    private ComplexPopup.EditListener<I> myListener = new ComplexPopup.EditListener<I>() {

        @Override
        public I beforeEdit() {
            return null; // no changes so, just let it ride.
        }

        @Override
        public void afterEdit(I newValue) {
            txtEditor.setText(thePopup.getValue().toString());
        }
    };

    /**
    * Creates new form ObjectSizeComboBox
    */
    public ComplexComboBox(I iniValue, Q iniPanel) {
        initComponents();
        thePopup = new ComplexPopup<I, Q>(iniValue, iniPanel, myListener);
    }

    public I getValue() {
        return thePopup.getValue();
    }

    public void setValue(I newValue) {
        thePopup.setValue(newValue);
    }

    private void initComponents() {

        txtEditor = new javax.swing.JTextField();
        btnShowPanel = new javax.swing.JButton();

        setBackground(new java.awt.Color(255, 255, 255));
        setMinimumSize(new java.awt.Dimension(40, 19));

        txtEditor.setEditable(false);
        txtEditor.setToolTipText("");
        txtEditor.setOpaque(false);

        btnShowPanel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/testpopupsubform/art/dropdownarrow.png"))); // NOI18N
        btnShowPanel.addActionListener(new java.awt.event.ActionListener() {
        public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnShowPanelActionPerformed(evt);
        }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(layout.createSequentialGroup()
            .addComponent(txtEditor, javax.swing.GroupLayout.DEFAULT_SIZE, 115, Short.MAX_VALUE)
            .addGap(1, 1, 1)
            .addComponent(btnShowPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE))
        );
        layout.setVerticalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addComponent(btnShowPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
        .addComponent(txtEditor)
        );
    }                        

    private void btnShowPanelActionPerformed(java.awt.event.ActionEvent evt) {                                             
            thePopup.show(txtEditor, 0, txtEditor.getHeight());
    }                                            

    private javax.swing.JButton btnShowPanel;
    private javax.swing.JTextField txtEditor;
}

This seems to work very well in a cell editor for a table, provided you make the row heights tall enough. That's no different than a regular JComboBox editor, though:

package complexcombobox;

import java.awt.Component;
import javax.swing.AbstractCellEditor;
import javax.swing.JTable;
import javax.swing.table.TableCellEditor;

/**
*
* @author MaskedCoder
*
* @param <E> the Element (Object) to be edited
* @param <P> the AbstractPopupPanel to be used to do the editing. 
*/
public class ComplexCellEditor<E, P extends AbstractPopupPanel<E>> extends AbstractCellEditor
            implements TableCellEditor {
    private ComplexComboBox<E, P> ComplexEditor;

    /*
    * Constructors
    */
    public ComplexCellEditor(E iniValue, P iniPanel) { 
        super();

        ComplexEditor = new ComplexComboBox<E, P>(iniValue, iniPanel);
    }

    /*
    * AbstractCellEditor Extensions
    */
    @Override
    public boolean stopCellEditing() {
            return super.stopCellEditing();
    }

    /*
    * TableCellEditor Implementation
    */
        @Override
    public E getCellEditorValue() {
            return ComplexEditor.getValue();
    }

    @Override
    public Component getTableCellEditorComponent(JTable iniTable, Object iniValue, boolean isSelected, int row, int column) {
            ComplexEditor.setValue((E) iniValue);

            return ComplexEditor;
    }
}

Three things I'm not entirely satisfied with: - I'd like to grab the JComboBox down-arrow icon from the system rather than relying on a project resource - I've had to pass through the AbstractPopupPanel instance which means that the coder has to create the panel themselves and then hand it to the constructor for ComplexComboBox or ComplexCellEditor . I'd rather have created the panel internally, in ComplexPopup and managed it myself. I ran into difficulty instantiating Generic constructors. Pass-through does allow the coder more control but I don't think that's needed and I'd like it to be optional at least.

My knowledge of Generics isn't advanced but this seems to do the trick.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM