Реализация узлов JTree с радио / флажками

Я пытаюсь создать элегантное древовидное представление, в котором определенные типы узлов отображаются в виде панелей, содержащих текст, переключатель и флажок. Ниже приведено изображение того, что у меня есть в настоящее время, и код, который его генерирует. Однако есть несколько проблем, из-за которых он становится грязным, и я не уверен, что это лучший способ их обойти.

Пример дерева

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) Кнопки / блоки реагируют только после того, как редактирование активируется путем однократного нажатия на узел. До этого кнопка / блок не светится при наведении мыши.

(2) Переключатели для каждой группы узлов под родителем находятся в одной группе кнопок. Но когда я выбираю один, визуальное представление другого не обновляется, чтобы отразить, что оно было отменено, пока я не щелкну где-нибудь в нем, чтобы "отредактировать" его.

(3) В общем, этот стандартный тип дерева, где узлы являются просто фиктивными объектами, а не фактическими компонентами, кажется, неправильно подходит для этого, но я не могу придумать лучшей альтернативы, которая позволяет мне группировать эти объекты, выбирать отдельные узлы (или листы или родители), и каждый лист содержит флажки / кнопки, которые работают должным образом.

Я открыт для предложений альтернативных решений.

РЕДАКТИРОВАТЬ:

Пробовал использовать Outline, который кажется ближе к тому, что я хочу, но с техническими проблемами. Я последовал примеру здесь. Вот что я получаю:

Набросок попытки 1

Как видите, кнопки не отображаются должным образом. Вот моя RowModel:

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

  }

}

1 ответ

Решение

Итак, я наконец-то понял, как добиться этого, основываясь на том, как JTable обрабатывает логические ячейки. Я создал эксклюзивное средство логического выбора для рисования JRadioButton и настроил узел дерева, чтобы убедиться, что эксклюзивный выбор поддерживается. Я также переопределил editStopped, чтобы обновить все ячейки в столбце, если одна из ячеек была отредактирована. Вероятно, есть способы улучшить это, но это работает для того, что мне нужно. Спасибо за руководство.

введите описание изображения здесь

Вот мой код:

Класс DatasetOutline

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 (модифицированная копия 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
Другие вопросы по тегам