使用 radio/check 个框实现 JTree 节点

Implementing JTree nodes with radio/check boxes

我正在尝试实现一种优雅的树形表示,其中某些类型的节点显示为包含文本、单选按钮和复选框的面板。下面是我目前拥有的图片以及生成它的代码。然而,有一些问题让它感觉很脏,我不确定解决这些问题的最佳方法。

public class DatasetTree extends JTree {

  public DatasetTree(String name) {
    super(new DatasetTreeModel(name));
    getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
    DatasetTreeCellRenderer renderer = new DatasetTreeCellRenderer();
    renderer.setOpenIcon(null);
    renderer.setClosedIcon(null);
    renderer.setLeafIcon(null);
    setCellRenderer(renderer);
    setEditable(true);
    PanelCellEditor editor = new PanelCellEditor(this, renderer);
    setCellEditor(editor);
    setShowsRootHandles(true);
    setRootVisible(false);
  }

  public DatasetTreeModel getDatasetModel() {
    return (DatasetTreeModel) treeModel;
  }

  public static class DatasetTreeCellRenderer extends DefaultTreeCellRenderer {

    public DatasetTreeCellRenderer() {

    }

    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel,
        boolean expanded, boolean leaf, int row, boolean hasFocus) {

      if ((value != null) && (value instanceof DatasetHandle)) {
        DatasetHandle h = (DatasetHandle) value;
        DatasetCellPanel line = new DatasetCellPanel(h);
        if (sel) {
          line.setBackground(getBackgroundSelectionColor());
          line.setForeground(getTextSelectionColor());
        } else {
          line.setBackground(getBackgroundNonSelectionColor());
          line.setForeground(getTextNonSelectionColor());
        }
        return line;
      }
      return super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
    }
  }

  public static class DatasetCellPanel extends JPanel {

    private final JLabel lblName, lblType, lblom, lbldata, lblimages, lblspectra;
    private boolean observable;
    private boolean orientable;

    private JRadioButton omButton;
    private JCheckBox dataSelectBox;

    /**
     * Create the panel.
     */
    public DatasetCellPanel(DatasetHandle h) {
      super();
      setBackground(Color.WHITE);
      FileData fd = h.getFileData();
      String name = fd.getFileName();
      boolean observable = (fd instanceof ObservableData);
      boolean orientable = (fd instanceof Orientable);
      String typeName = fd.getClass().getSimpleName();
      lblName = new JLabel("");
      lblType = new JLabel("");
      lblom = new JLabel("[om]");
      lbldata = new JLabel("[data]");
      lblimages = new JLabel("[images]");
      lblspectra = new JLabel("[spectra]");

      JRadioButton omButton = new JRadioButton("");
      JCheckBox dataSelectBox = new JCheckBox("");

      setLayout(new BoxLayout(this, BoxLayout.X_AXIS));

      lblName.setText(name);
      lblName.setMinimumSize(new Dimension(100, 8));
      lblName.setPreferredSize(new Dimension(100, 16));
      lblName.setMaximumSize(new Dimension(100, 64));
      add(lblName);
      add(Box.createRigidArea(new Dimension(5, 0)));

      lblType.setText(typeName);
      lblType.setMinimumSize(new Dimension(100, 8));
      lblType.setPreferredSize(new Dimension(100, 16));
      lblType.setMaximumSize(new Dimension(100, 64));
      add(lblType);
      add(Box.createRigidArea(new Dimension(5, 0)));

      if (orientable) {
        omButton = h.getLatticeButton();
      } else {
        lblom.setForeground(UIManager.getColor("Label.disabledForeground"));
        omButton.setEnabled(false);
      }
      add(lblom);
      add(omButton);
      add(Box.createRigidArea(new Dimension(5, 0)));

      if (observable) {
        dataSelectBox = h.getDataButton();
      } else {
        lbldata.setForeground(UIManager.getColor("Label.disabledForeground"));
        dataSelectBox.setEnabled(false);
      }
      add(lbldata);
      add(dataSelectBox);
      add(Box.createRigidArea(new Dimension(5, 0)));

      add(lblimages);
      add(Box.createRigidArea(new Dimension(5, 0)));
      add(lblspectra);

    }

    public void addListeners(EventListener l) {

    }

    @Override
    public void setForeground(Color fg) {
      if (lblName != null) {
        lblName.setForeground(fg);
      }
      if (lblType != null) {
        lblType.setForeground(fg);
      }
      if (observable && (lbldata != null)) {
        lbldata.setForeground(fg);
      }
      if (orientable && (lblom != null)) {
        lblom.setForeground(fg);
      }
      if (lblimages != null) {
        lblimages.setForeground(fg);
      }
      if (lblspectra != null) {
        lblspectra.setForeground(fg);
      }
      super.setForeground(fg);
    }

    @Override
    public void setBackground(Color bg) {
      if (omButton != null) {
        omButton.setBackground(bg);
      }
      if (dataSelectBox != null) {
        dataSelectBox.setBackground(bg);
      }
      super.setBackground(bg);
    }

  }

  public static class PanelCellEditor extends AbstractCellEditor implements TreeCellEditor {

    Object value;
    private JTree tree;
    private DefaultTreeCellRenderer renderer;

    public PanelCellEditor(JTree tree, DefaultTreeCellRenderer renderer) {
      this.tree = tree;
      this.renderer = renderer;
    }

    @Override
    public Object getCellEditorValue() {
      return value;
    }

    // FIXME: Redraw all in group when one is edited
    @Override
    public Component getTreeCellEditorComponent(JTree tree, Object value, boolean sel,
        boolean expanded, boolean leaf, int row) {
      this.value = value;
      if ((value != null) && (value instanceof DatasetHandle)) {
        DatasetHandle h = (DatasetHandle) value;
        DatasetCellPanel line = new DatasetCellPanel(h);
        if (sel) {
          line.setBackground(renderer.getBackgroundSelectionColor());
          line.setForeground(renderer.getTextSelectionColor());
        } else {
          line.setBackground(renderer.getBackgroundNonSelectionColor());
          line.setForeground(renderer.getTextNonSelectionColor());
        }
        return line;
      }
      return renderer.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, false);
    }
  }

}

(1) buttons/boxes 仅在通过单击节点一次启用编辑后才响应。在此之前,button/box 不会在鼠标悬停时发光。

(2)父节点下每组节点的单选按钮都在一个按钮组中。但是当我 select 一个时,另一个的视觉表示不会更新以反映它已被删除 select,直到我点击它的某个地方 'edit' 它。

(3) 一般来说,这种标准类型的树,其中的节点只是虚拟对象,而不是实际组件,似乎不适合这个,但我想不出一个更好的选择来让我分组这些对象,select 个单独的节点(叶节点或父节点),并让每个叶节点包含检查 boxes/buttons 正常工作。

我乐于接受替代解决方案的建议。

编辑:

尝试使用 Outline,这似乎更接近我想要的,但存在技术问题。我按照示例 here 进行了操作。这是我得到的:

如您所见,按钮显示不正确。这是我的行模型:

public class DatasetOutlineRowModel implements RowModel {

  @Override
  public Class getColumnClass(int column) {
    switch (column) {
      case 0:
        return JRadioButton.class;
      case 1:
        return JCheckBox.class;
      case 2:
        return String.class;
      case 3:
        return String.class;
      default:
        assert false;
    }
    return null;
  }

  @Override
  public int getColumnCount() {
    return 4;
  }

  @Override
  public String getColumnName(int column) {
    switch (column) {
      case 0:
        return "OM";
      case 1:
        return "Data";
      case 2:
        return "Images";
      case 3:
        return "Spectra";
      default:
        assert false;
    }
    return null;
  }

  @Override
  public Object getValueFor(Object node, int column) {
    if (!(node instanceof DatasetHandle))
      return null;
    DatasetHandle handle = (DatasetHandle) node;
    switch (column) {
      case 0:
        return handle.getLatticeButton();
      case 1:
        return handle.getDataButton();
      case 2:
        return "";
      case 3:
        return "";
      default:
        assert false;
    }
    return null;
  }

  @Override
  public boolean isCellEditable(Object arg0, int arg1) {
    return false;
  }

  @Override
  public void setValueFor(Object arg0, int arg1, Object arg2) {
    // TODO Auto-generated method stub

  }

}

好的,所以我终于想出了如何根据 JTable 处理布尔单元格的方式实现这一点。我创建了一个独占布尔选择渲染器来绘制 JR​​adioButton 并设置树节点以确保保留独占选择。如果编辑了其中一个单元格,我还覆盖了 editingStopped 以更新列中的所有单元格。可能有一些方法可以改进这一点,但它可以满足我的需要。感谢指导。

这是我的代码:

数据集大纲class

public class DatasetOutline extends Outline {

  public DatasetOutline(DatasetTreeModel mdl) {
    setRenderDataProvider(new DatasetRenderProvider());
    setRootVisible(false);
    setShowGrid(false);
    setIntercellSpacing(new Dimension(0, 0));
    setModel(DefaultOutlineModel.createOutlineModel(mdl, new DatasetOutlineRowModel(), true,
        "Dataset"));
    getColumnModel().getColumn(1).setCellRenderer(new ExclusiveBooleanRenderer());
    getColumnModel().getColumn(1).setCellEditor(new ExclusiveBooleanEditor());
    // [snip]
    getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
  }

  // Update the entire column of the conditional boolean if one is changed
  @Override
  public void editingStopped(ChangeEvent e) {
    super.editingStopped(e);
    if (e.getSource() instanceof ExclusiveBooleanEditor) {
      tableChanged(new TableModelEvent(getModel(), 0, getRowCount(), 1, TableModelEvent.UPDATE));
    }
  }
}

DatasetOutlineRowModel class

public class DatasetOutlineRowModel implements RowModel {

  @Override
  public Class getColumnClass(int column) {
    switch (column) {
      case 0:
        return Boolean.class;
      case 1:
        return Boolean.class;
      case 2:
        return String.class;
      case 3:
        return String.class;
      default:
        assert false;
    }
    return null;
  }

 // [snip]

  @Override
  public Object getValueFor(Object node, int column) {
    if (!(node instanceof DatasetHandle))
      return null;
    DatasetHandle handle = (DatasetHandle) node;
    switch (column) {
      case 0:
        return handle.isLatticeSelected();
      case 1:
        return handle.isSelected();
      case 2:
        return "";
      case 3:
        return "";
      default:
        assert false;
    }
    return null;
  }

  @Override
  public boolean isCellEditable(Object node, int column) {
    if (column > 1)
      return false;
    if (node instanceof DatasetHandle)
      return true;
    return false;
  }

  @Override
  public void setValueFor(Object node, int column, Object value) {
    if (!(node instanceof DatasetHandle))
      return;
    DatasetHandle handle = (DatasetHandle) node;
    if (column == 0) {
      handle.setLatticeSelected((Boolean) value);
    }
    if (column == 1) {
      handle.setSelected((Boolean) value);
    }

  }

}

ExclusiveBooleanEditor class(DefaultCellRenderer 的修改副本)

public class ExclusiveBooleanEditor extends AbstractCellEditor implements TableCellEditor,
    TreeCellEditor {

  //
  // Instance Variables
  //

  /** The Swing component being edited. */
  protected JComponent editorComponent;
  /**
   * The delegate class which handles all methods sent from the <code>CellEditor</code>.
   */
  protected EditorDelegate delegate;
  /**
   * An integer specifying the number of clicks needed to start editing. Even if
   * <code>clickCountToStart</code> is defined as zero, it will not initiate until a click occurs.
   */
  protected int clickCountToStart = 1;

  //
  // Constructors
  //

  public ExclusiveBooleanEditor() {
    this(new JRadioButton());
    JRadioButton radioButton = (JRadioButton) getComponent();
    radioButton.setHorizontalAlignment(JRadioButton.CENTER);
  }

  public ExclusiveBooleanEditor(final JRadioButton radioButton) {
    editorComponent = radioButton;
    delegate = new EditorDelegate() {
      // FIXME replace
      @Override
      public void setValue(Object value) {
        boolean selected = false;
        if (value instanceof Boolean) {
          selected = ((Boolean) value).booleanValue();
        } else if (value instanceof String) {
          selected = value.equals("true");
        }
        radioButton.setSelected(selected);
      }

      @Override
      public Object getCellEditorValue() {
        return Boolean.valueOf(radioButton.isSelected());
      }
    };
    radioButton.addActionListener(delegate);
    radioButton.setRequestFocusEnabled(false);
  }

  /**
   * Returns a reference to the editor component.
   *
   * @return the editor <code>Component</code>
   */
  public Component getComponent() {
    return editorComponent;
  }

  //
  // Modifying
  //

  /**
   * Specifies the number of clicks needed to start editing.
   *
   * @param count an int specifying the number of clicks needed to start editing
   * @see #getClickCountToStart
   */
  public void setClickCountToStart(int count) {
    clickCountToStart = count;
  }

  /**
   * Returns the number of clicks needed to start editing.
   * 
   * @return the number of clicks needed to start editing
   */
  public int getClickCountToStart() {
    return clickCountToStart;
  }

  //
  // Override the implementations of the superclass, forwarding all methods
  // from the CellEditor interface to our delegate.
  //

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#getCellEditorValue
   */
  @Override
  public Object getCellEditorValue() {
    return delegate.getCellEditorValue();
  }

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#isCellEditable(EventObject)
   */
  @Override
  public boolean isCellEditable(EventObject anEvent) {
    return delegate.isCellEditable(anEvent);
  }

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#shouldSelectCell(EventObject)
   */
  @Override
  public boolean shouldSelectCell(EventObject anEvent) {
    return delegate.shouldSelectCell(anEvent);
  }

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#stopCellEditing
   */
  @Override
  public boolean stopCellEditing() {
    return delegate.stopCellEditing();
  }

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#cancelCellEditing
   */
  @Override
  public void cancelCellEditing() {
    delegate.cancelCellEditing();
  }

  //
  // Implementing the TreeCellEditor Interface
  //

  /** Implements the <code>TreeCellEditor</code> interface. */
  @Override
  public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected,
      boolean expanded, boolean leaf, int row) {
    String stringValue = tree.convertValueToText(value, isSelected, expanded, leaf, row, false);

    delegate.setValue(stringValue);
    return editorComponent;
  }

  //
  // Implementing the CellEditor Interface
  //
  /** Implements the <code>TableCellEditor</code> interface. */
  @Override
  public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected,
      int row, int column) {
    delegate.setValue(value);
    if ((editorComponent instanceof JCheckBox) || (editorComponent instanceof JRadioButton)) {
      // in order to avoid a "flashing" effect when clicking a checkbox
      // in a table, it is important for the editor to have as a border
      // the same border that the renderer has, and have as the background
      // the same color as the renderer has. This is primarily only
      // needed for JCheckBox since this editor doesn't fill all the
      // visual space of the table cell, unlike a text field.
      TableCellRenderer renderer = table.getCellRenderer(row, column);
      Component c =
          renderer.getTableCellRendererComponent(table, value, isSelected, true, row, column);
      if (c != null) {
        editorComponent.setOpaque(true);
        editorComponent.setBackground(c.getBackground());
        if (c instanceof JComponent) {
          editorComponent.setBorder(((JComponent) c).getBorder());
        }
      } else {
        editorComponent.setOpaque(false);
      }
    }
    return editorComponent;
  }


  //
  // Protected EditorDelegate class
  //

  /**
   * The protected <code>EditorDelegate</code> class.
   */
  protected class EditorDelegate implements ActionListener, ItemListener, Serializable {

    /** The value of this cell. */
    protected Object value;

    /**
     * Returns the value of this cell.
     * 
     * @return the value of this cell
     */
    public Object getCellEditorValue() {
      return value;
    }

    /**
     * Sets the value of this cell.
     * 
     * @param value the new value of this cell
     */
    public void setValue(Object value) {
      this.value = value;
    }

    /**
     * Returns true if <code>anEvent</code> is <b>not</b> a <code>MouseEvent</code>. Otherwise, it
     * returns true if the necessary number of clicks have occurred, and returns false otherwise.
     *
     * @param anEvent the event
     * @return true if cell is ready for editing, false otherwise
     * @see #setClickCountToStart
     * @see #shouldSelectCell
     */
    public boolean isCellEditable(EventObject anEvent) {
      if (anEvent instanceof MouseEvent) {
        return ((MouseEvent) anEvent).getClickCount() >= clickCountToStart;
      }
      return true;
    }

    /**
     * Returns true to indicate that the editing cell may be selected.
     *
     * @param anEvent the event
     * @return true
     * @see #isCellEditable
     */
    public boolean shouldSelectCell(EventObject anEvent) {
      return true;
    }

    /**
     * Returns true to indicate that editing has begun.
     *
     * @param anEvent the event
     */
    public boolean startCellEditing(EventObject anEvent) {
      return true;
    }

    /**
     * Stops editing and returns true to indicate that editing has stopped. This method calls
     * <code>fireEditingStopped</code>.
     *
     * @return true
     */
    public boolean stopCellEditing() {
      fireEditingStopped();
      return true;
    }

    /**
     * Cancels editing. This method calls <code>fireEditingCanceled</code>.
     */
    public void cancelCellEditing() {
      fireEditingCanceled();
    }

    /**
     * When an action is performed, editing is ended.
     * 
     * @param e the action event
     * @see #stopCellEditing
     */
    @Override
    public void actionPerformed(ActionEvent e) {
      ExclusiveBooleanEditor.this.stopCellEditing();
    }

    /**
     * When an item's state changes, editing is ended.
     * 
     * @param e the action event
     * @see #stopCellEditing
     */
    @Override
    public void itemStateChanged(ItemEvent e) {
      ExclusiveBooleanEditor.this.stopCellEditing();
    }
  }

  public static class ExclusiveBooleanRenderer extends JRadioButton implements TableCellRenderer,
      UIResource {
    private static final Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
    private static final JLabel emptyLabel = new JLabel("");

    public ExclusiveBooleanRenderer() {
      super();
      setHorizontalAlignment(JRadioButton.CENTER);
      setBorderPainted(true);
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
        boolean hasFocus, int row, int column) {

      // Don't draw if it is not changeable
      if (value == null) {
        if (isSelected) {
          emptyLabel.setForeground(table.getSelectionForeground());
          emptyLabel.setBackground(table.getSelectionBackground());
        } else {
          emptyLabel.setForeground(table.getForeground());
          emptyLabel.setBackground(table.getBackground());
        }

        return emptyLabel;
      }
      if (isSelected) {
        setForeground(table.getSelectionForeground());
        super.setBackground(table.getSelectionBackground());
      } else {
        setForeground(table.getForeground());
        setBackground(table.getBackground());
      }
      setSelected((value != null && ((Boolean) value).booleanValue()));

      if (hasFocus) {
        setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
      } else {
        setBorder(noFocusBorder);
      }

      return this;
    }
  }

} // End of class JCellEditor