---
category: swing
folder: CheckBoxNodeEditor
title: JTreeのすべてのノードにJCheckBoxを追加する
tags: [JTree, JCheckBox, TreeCellRenderer, TreeCellEditor, TreeModelListener, Icon]
author: aterai
pubdate: 2012-02-06T14:38:59+09:00
description: JTreeのすべてのノードに編集可能なJCheckBoxを追加します。
image: https://lh4.googleusercontent.com/-DK6aW3VNikg/TygxL3j8UoI/AAAAAAAABIw/6_9FyPe4v7U/s800/CheckBoxNodeEditor.png
hreflang:
    href: https://java-swing-tips.blogspot.com/2012/02/jcheckbox-node-jtree.html
    lang: en
---
* 概要 [#summary]
`JTree`のすべてのノードに編集可能な`JCheckBox`を追加します。

#download(https://lh4.googleusercontent.com/-DK6aW3VNikg/TygxL3j8UoI/AAAAAAAABIw/6_9FyPe4v7U/s800/CheckBoxNodeEditor.png)

* サンプルコード [#sourcecode]
#code(link){{
class CheckBoxNodeEditor extends TriStateCheckBox implements TreeCellEditor {
  private DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
  private final JPanel panel = new JPanel(new BorderLayout());
  private String str = null;
  public CheckBoxNodeEditor() {
    super();
    this.addActionListener(new ActionListener() {
      @Override public void actionPerformed(ActionEvent e) {
        // System.out.println("actionPerformed: stopCellEditing");
        stopCellEditing();
      }
    });
    panel.setFocusable(false);
    panel.setRequestFocusEnabled(false);
    panel.setOpaque(false);
    panel.add(this, BorderLayout.WEST);
    this.setOpaque(false);
  }

  @Override public Component getTreeCellEditorComponent(
      JTree tree, Object value, boolean isSelected,
      boolean expanded, boolean leaf, int row) {
    JLabel l = (JLabel) renderer.getTreeCellRendererComponent(
      tree, value, true, expanded, leaf, row, true);
    l.setFont(tree.getFont());
    if (value instanceof DefaultMutableTreeNode) {
      this.setEnabled(tree.isEnabled());
      this.setFont(tree.getFont());
      Object userObject = ((DefaultMutableTreeNode) value).getUserObject();
      if (userObject instanceof CheckBoxNode) {
        CheckBoxNode node = (CheckBoxNode) userObject;
        if (node.status == Status.INDETERMINATE) {
          setIcon(new IndeterminateIcon());
        } else {
          setIcon(null);
        }
        l.setText(node.label);
        setSelected(node.status == Status.SELECTED);
        str = node.label;
      }
      // panel.add(this, BorderLayout.WEST);
      panel.add(l);
      return panel;
    }
    return l;
  }

  @Override public Object getCellEditorValue() {
    return new CheckBoxNode(str, isSelected() ? Status.SELECTED : Status.DESELECTED);
  }

  @Override public boolean isCellEditable(EventObject e) {
    if (e instanceof MouseEvent && e.getSource() instanceof JTree) {
      MouseEvent me = (MouseEvent) e;
      JTree tree = (JTree) e.getSource();
      TreePath path = tree.getPathForLocation(me.getX(), me.getY());
      Rectangle r = tree.getPathBounds(path);
      if (r == null) {
        return false;
      }
      Dimension d = getPreferredSize();
      r.setSize(new Dimension(d.width, r.height));
      if (r.contains(me.getPoint())) {
        return true;
      }
    }
    return false;
  }

  @Override public void updateUI() {
    super.updateUI();
    setName("Tree.cellEditor");
    if (panel != null) {
      //panel.removeAll(); // ??? Change to Nimbus LnF, JDK 1.6.0
      panel.updateUI();
      //panel.add(this, BorderLayout.WEST);
    }
    // ???#1: JDK 1.6.0 bug??? @see 1.7.0 DefaultTreeCellRenderer#updateUI()
    // if (System.getProperty("java.version").startsWith("1.6.0")) {
    //   renderer = new DefaultTreeCellRenderer();
    // }
  }
// ...
}}

* 解説 [#explanation]
- `JCheckBox`を継承する`TreeCellEditor`、`TreeCellRenderer`を作成
-- `TreeCellEditor#getTreeCellEditorComponent(...)`などはこの`JCheckBox`、`JLabel`(文字列、アイコン)などを含む`JPanel`を生成して返す
- マウスクリックなどでノードのチェック状態が変更されたら子ノードと親ノードの選択状態も更新する必要があるので、`TreeModelListener`をモデルに設定して監視
- `JPanel`を継承する`TreeCellEditor`と`TreeCellRenderer`でも、`JDK 1.7.0`、`JDK 1.6.0_30`などでは問題なく動作可能

#code{{
class CheckBoxNodeRenderer extends JPanel implements TreeCellRenderer {
  private DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
  private final TriStateCheckBox check = new TriStateCheckBox();
  public CheckBoxNodeRenderer() {
    super(new BorderLayout());
    String uiName = getUI().getClass().getName();
    if (uiName.contains("Synth") && System.getProperty("java.version").startsWith("1.7.0")) {
      System.out.println("XXX: FocusBorder bug?, JDK 1.7.0, Nimbus start LnF");
      renderer.setBackgroundSelectionColor(new Color(0x0, true));
    }
    setFocusable(false);
    setRequestFocusEnabled(false);
    setOpaque(false);
    add(check, BorderLayout.WEST);
    check.setOpaque(false);
  }
  @Override public Component getTreeCellRendererComponent(
      JTree tree, Object value, boolean selected,
      boolean expanded, boolean leaf, int row, boolean hasFocus) {
    JLabel l = (JLabel) renderer.getTreeCellRendererComponent(
      tree, value, selected, expanded, leaf, row, hasFocus);
    l.setFont(tree.getFont());
    if (value instanceof DefaultMutableTreeNode) {
      check.setEnabled(tree.isEnabled());
      check.setFont(tree.getFont());
      Object userObject = ((DefaultMutableTreeNode) value).getUserObject();
      if (userObject instanceof CheckBoxNode) {
        CheckBoxNode node = (CheckBoxNode) userObject;
        if (node.status == Status.INDETERMINATE) {
          check.setIcon(new IndeterminateIcon());
        } else {
          check.setIcon(null);
        }
        l.setText(node.label);
        check.setSelected(node.status == Status.SELECTED);
      }
      add(l);
      return this;
    }
    return l;
  }
  @Override public void updateUI() {
    super.updateUI();
    if (check != null) {
      // removeAll(); // ??? Change to Nimbus LnF, JDK 1.6.0
      check.updateUI();
      // add(check, BorderLayout.WEST);
    }
    setName("Tree.cellRenderer");
    // ???#1: JDK 1.6.0 bug??? @see 1.7.0 DefaultTreeCellRenderer#updateUI()
    // if (System.getProperty("java.version").startsWith("1.6.0")) {
    //   renderer = new DefaultTreeCellRenderer();
    // }
  }
}
}}

----
- `JDK 1.6.0`で`LookAndFeel`を`Nimbus`などに変更すると、セルエディタなどが更新されず、表示が不正になる場合があるので、`JTree#updateUI()`を以下のようにオーバーライドして回避

#code{{
JTree tree = new JTree() {
  @Override public void updateUI() {
    setCellRenderer(null);
    setCellEditor(null);
    super.updateUI();
    //???#1: JDK 1.6.0 bug??? Nimbus LnF
    setCellRenderer(new CheckBoxNodeRenderer());
    setCellEditor(new CheckBoxNodeEditor());
  }
};
}}

- `JDK 1.7.0`で、初期`LookAndFeel`を`Nimbus`にすると、ノードにフォーカスがある場合のグラデーション描画がノードの背景色で塗りつぶされてしまう?のを、以下のようにして回避
-- &ref(https://lh3.googleusercontent.com/-DQgyx52YcsQ/T6sfFSWGIpI/AAAAAAAABMc/jAx8XeuMeWI/s800/CheckBoxNodeEditor1.png);

#code{{
public CheckBoxNodeRenderer() {
  super();
  String uiName = getUI().getClass().getName();
  if (uiName.contains("Synth") && System.getProperty("java.version").startsWith("1.7.0")) {
    System.out.println("XXX: FocusBorder bug?, JDK 1.7.0, Nimbus start LnF");
    renderer.setBackgroundSelectionColor(new Color(0x0, true));
  }
// ...
}}

- %%`JDK 1.7.0`で、ノードのチェックボックスをクリックしても、初回だけ反応しない%% `1.7.0_60`で修正されている
-- `JDK 1.6.0_30`などは問題なし
-- `TreeCellEditor#isCellEditable()`をオーバーライドして、初回のみセルエディタのサイズを以下のように設定
-- `JDK 1.8.0`では、修正されている
-- [https://bugs.openjdk.java.net/browse/JDK-8023474 Bug ID: JDK-8023474 First mousepress doesn't start editing in JTree]
-- [https://bugs.openjdk.org/browse/JDK-8023474 Bug ID: JDK-8023474 First mousepress doesn't start editing in JTree]

#code{{
if (isFirstTime && System.getProperty("java.version").startsWith("1.7.0")) {
  System.out.println("XXX: Java 7, only on first run\n" + getBounds());
  setBounds(new Rectangle(0, 0, d.width, r.height));
}
}}

* 参考リンク [#reference]
- [[JTreeの葉ノードをJCheckBoxにする>Swing/CheckBoxNodeTree]]
- [[JCheckBoxに不定状態のアイコンを追加する>Swing/TriStateCheckBox]]
- [[JCheckBox付きJTreeでディレクトリ構造を表示>Swing/FileSystemTreeWithCheckBox]]

* コメント [#comment]
#comment
- 親ノードまでではなく、ルートノードまで不定状態の変更を行うように修正。 -- &user(aterai); &new{2012-03-23 (金) 17:43:32};

#comment