TreeCellRenderer 中的标签宽度不正确
Incorrect label width in TreeCellRenderer
我得到了一个带有定制 TreeCellRenderer
的 JTree
。
此渲染器是一个包含复选框和标签的面板。
虽然每个节点的标签文本是固定的(在 DefaultMutableTreeNode
的 UserObject 中指定),但此文本可能会或可能不会是粗体。这取决于复选框的状态。
取消复选框后,select 标签文本不再是粗体,但其宽度保持不变(太宽)。
类似的,当select选中复选框时,文本报告为粗体但标签没有放大。
这会导致文本被截断。
现实生活中的情况稍微复杂一些,但下面是一个完整的例子。
为了重现问题:
- Select点击文本的第一个节点
- 按 space 栏取消 select 复选框。文本不是粗体但标签太大。
- 单击第一个节点上的展开按钮。标签宽度调整
- 按 space 栏到 select 组合框。文本变为粗体但被截断。
我尝试插入对 invalidate
、repaint
等的多个调用,但没有解决问题。
默认外观和系统 (Windows) 外观都会出现此问题。
import java.awt.*;
import javax.swing.*;
@SuppressWarnings("serial")
public class TestFrame extends JFrame
{
public TestFrame()
{
getContentPane().setLayout(new GridBagLayout());
setDefaultCloseOperation(EXIT_ON_CLOSE);
setTitle("Test TreeCellRenderer");
JScrollPane tree_pane;
tree_pane = new JScrollPane();
tree_pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
tree_pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
tree_pane.setPreferredSize(new Dimension(300, 200));
TestTree tree;
tree = new TestTree();
tree_pane.getViewport().add(tree, null);
GridBagConstraints constraints;
constraints = new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.NORTH,
GridBagConstraints.BOTH, new Insets(8, 8, 8, 8), 0, 0);
getContentPane().add(tree_pane, constraints);
pack();
setMinimumSize(getPreferredSize());
}
public static void main(String[] args)
{
try
{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
TestFrame frame;
frame = new TestFrame();
frame.setVisible(true);
}
catch (Exception exception)
{
exception.printStackTrace();
}
} // main
} // class TestFrame
这个 class 实现了我的树:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.*;
@SuppressWarnings("serial")
public class TestTree extends JTree
{
// The width of the checkbox within the renderer
// We need it when a node is clicked in order to check what part is exactly underneath the mouse
public int checkboxWidth;
public TestTree()
{
// Initialize object
super(getNodes());
setRootVisible(false);
setShowsRootHandles(true);
setCellRenderer(new MyCellRenderer());
addMouseListener(new TreeMouseManager());
addKeyListener(new TreeKeyManager());
} // constructor
private void toggleCheckBox(TreePath treePath)
{
// Determine node being toggled
Object[] path;
DefaultMutableTreeNode node;
NodeInfo info;
path = treePath.getPath();
node = (DefaultMutableTreeNode)path[path.length - 1];
info = (NodeInfo)node.getUserObject();
// Toggle selection
info.checked = !info.checked;
repaint();
} // toggleCheckBox
private class TreeMouseManager extends MouseAdapter
{
@Override
public void mouseClicked(MouseEvent event)
{
// Determine node corresponding to location
TreePath treePath;
treePath = getPathForLocation(event.getX(), event.getY());
if (treePath == null)
return;
// Manage only single click with left button
if ((event.getClickCount() != 1) || (event.getButton() != MouseEvent.BUTTON1))
return;
// Determine horizontal position of checkbox
BasicTreeUI ui;
int depth;
int leftIndent;
int rightIndent;
int checkboxLeft;
int checkboxRight;
ui = (BasicTreeUI)getUI();
depth = treePath.getPathCount();
leftIndent = ui.getLeftChildIndent();
rightIndent = ui.getRightChildIndent();
checkboxLeft = (depth - 1) * (leftIndent + rightIndent);
checkboxRight = checkboxLeft + checkboxWidth - 1;
// Ignore if not clicked on checkbox
int x;
x = event.getX();
if ((x < checkboxLeft) || (x > checkboxRight))
return;
// Toggle checkbox
toggleCheckBox(treePath);
} // mouseClicked
} // class TreeMouseManager
private class TreeKeyManager extends KeyAdapter
{
@Override
public void keyPressed(KeyEvent event)
{
// Determine selected element
TreePath treePath;
treePath = getSelectionPath();
if (treePath == null)
return;
// Manage event for this element
if (event.getKeyCode() == KeyEvent.VK_SPACE)
toggleCheckBox(treePath);
} // keyPressed
} // class TreeKeyManager
private class MyCellRenderer extends JPanel implements TreeCellRenderer
{
public MyCellRenderer()
{
// Create components
checkbox = new JCheckBox();
checkbox.setBorder(null);
checkbox.setOpaque(false);
label = new JLabel();
label.setBorder(new EmptyBorder(new Insets(0, 2, 0, 2)));
// Initialize panel
GridBagConstraints constraints;
setLayout(new GridBagLayout());
setOpaque(false);
constraints = new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
add(checkbox, constraints);
constraints = new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
add(label, constraints);
// Save the width of the checkbox
// We need it when the mouse is clicked on a node
checkboxWidth = (int)checkbox.getPreferredSize().getWidth();
} // constructor
@Override
public Component getTreeCellRendererComponent(JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus)
{
// Make data accessible
// Ignore if it's the root node
DefaultMutableTreeNode node;
NodeInfo info;
node = (DefaultMutableTreeNode)value;
if (node.getUserObject() instanceof NodeInfo)
info = (NodeInfo)node.getUserObject();
else
return (this);
// Determine font
Font font;
font = label.getFont();
if (info.checked)
font = font.deriveFont(font.getStyle() | Font.BOLD);
else
font = font.deriveFont(font.getStyle() & ~Font.BOLD);
// Configure components
checkbox.setSelected(info.checked);
label.setText(info.name);
label.setOpaque(selected);
label.setFont(font);
if (selected)
{
label.setBackground(SystemColor.textHighlight);
label.setForeground(SystemColor.textHighlightText);
}
else
{
label.setBackground(SystemColor.text);
label.setForeground(SystemColor.textText);
}
// Make sure everything is painted correctly
label.invalidate();
checkbox.invalidate();
invalidate();
// Done
return (this);
} // getTreeCellRendererComponent
private JCheckBox checkbox;
private JLabel label;
} // class MyCellRenderer
private static DefaultMutableTreeNode getNodes()
{
// Create root
DefaultMutableTreeNode root;
root = new DefaultMutableTreeNode("root");
// Create first level children
DefaultMutableTreeNode first;
DefaultMutableTreeNode second;
DefaultMutableTreeNode third;
NodeInfo info;
info = new NodeInfo();
info.name = "This is the first node";
info.checked = true;
first = new DefaultMutableTreeNode(info);
info = new NodeInfo();
info.name = "And this is the second";
info.checked = false;
second = new DefaultMutableTreeNode(info);
info = new NodeInfo();
info.name = "Finally, the third";
info.checked = false;
third = new DefaultMutableTreeNode(info);
root.add(first);
root.add(second);
root.add(third);
// Add second level children
info = new NodeInfo();
info.name = "Second level node";
info.checked = true;
first.add(new DefaultMutableTreeNode(info));
info = new NodeInfo();
info.name = "This is another one";
info.checked = false;
first.add(new DefaultMutableTreeNode(info));
info = new NodeInfo();
info.name = "And this is the last one";
info.checked = true;
first.add(new DefaultMutableTreeNode(info));
// Done
return (root);
} // getNodes
private static class NodeInfo
{
public String name;
public boolean checked;
}
} // class TestTree
更新
在 getTreeCellRendererComponent
内,我尝试获得首选尺寸。
他们看起来还不错。当 select 选中复选框时,标签和面板本身的首选尺寸都会增加。当取消select复选框时,它们会减少。
感谢这个问题Change JTree row height resizing behavior when rendering的回答,我自己解决了这个问题:
private void toggleCheckBox(TreePath treePath)
{
// Determine node being toggled
Object[] path;
DefaultMutableTreeNode node;
NodeInfo info;
path = treePath.getPath();
node = (DefaultMutableTreeNode)path[path.length - 1];
info = (NodeInfo)node.getUserObject();
// Toggle selection
info.checked = !info.checked;
// Make sure tree recalculates width of the nodes
BasicTreeUI ui = (BasicTreeUI)getUI();
try
{
Method method = BasicTreeUI.class.getDeclaredMethod("configureLayoutCache");
method.setAccessible(true);
method.invoke(ui);
}
catch (Exception e1)
{
e1.printStackTrace();
}
} // toggleCheckBox
我得到了一个带有定制 TreeCellRenderer
的 JTree
。
此渲染器是一个包含复选框和标签的面板。
虽然每个节点的标签文本是固定的(在 DefaultMutableTreeNode
的 UserObject 中指定),但此文本可能会或可能不会是粗体。这取决于复选框的状态。
取消复选框后,select 标签文本不再是粗体,但其宽度保持不变(太宽)。
类似的,当select选中复选框时,文本报告为粗体但标签没有放大。
这会导致文本被截断。
现实生活中的情况稍微复杂一些,但下面是一个完整的例子。
为了重现问题:
- Select点击文本的第一个节点
- 按 space 栏取消 select 复选框。文本不是粗体但标签太大。
- 单击第一个节点上的展开按钮。标签宽度调整
- 按 space 栏到 select 组合框。文本变为粗体但被截断。
我尝试插入对 invalidate
、repaint
等的多个调用,但没有解决问题。
默认外观和系统 (Windows) 外观都会出现此问题。
import java.awt.*;
import javax.swing.*;
@SuppressWarnings("serial")
public class TestFrame extends JFrame
{
public TestFrame()
{
getContentPane().setLayout(new GridBagLayout());
setDefaultCloseOperation(EXIT_ON_CLOSE);
setTitle("Test TreeCellRenderer");
JScrollPane tree_pane;
tree_pane = new JScrollPane();
tree_pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
tree_pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
tree_pane.setPreferredSize(new Dimension(300, 200));
TestTree tree;
tree = new TestTree();
tree_pane.getViewport().add(tree, null);
GridBagConstraints constraints;
constraints = new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.NORTH,
GridBagConstraints.BOTH, new Insets(8, 8, 8, 8), 0, 0);
getContentPane().add(tree_pane, constraints);
pack();
setMinimumSize(getPreferredSize());
}
public static void main(String[] args)
{
try
{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
TestFrame frame;
frame = new TestFrame();
frame.setVisible(true);
}
catch (Exception exception)
{
exception.printStackTrace();
}
} // main
} // class TestFrame
这个 class 实现了我的树:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.*;
@SuppressWarnings("serial")
public class TestTree extends JTree
{
// The width of the checkbox within the renderer
// We need it when a node is clicked in order to check what part is exactly underneath the mouse
public int checkboxWidth;
public TestTree()
{
// Initialize object
super(getNodes());
setRootVisible(false);
setShowsRootHandles(true);
setCellRenderer(new MyCellRenderer());
addMouseListener(new TreeMouseManager());
addKeyListener(new TreeKeyManager());
} // constructor
private void toggleCheckBox(TreePath treePath)
{
// Determine node being toggled
Object[] path;
DefaultMutableTreeNode node;
NodeInfo info;
path = treePath.getPath();
node = (DefaultMutableTreeNode)path[path.length - 1];
info = (NodeInfo)node.getUserObject();
// Toggle selection
info.checked = !info.checked;
repaint();
} // toggleCheckBox
private class TreeMouseManager extends MouseAdapter
{
@Override
public void mouseClicked(MouseEvent event)
{
// Determine node corresponding to location
TreePath treePath;
treePath = getPathForLocation(event.getX(), event.getY());
if (treePath == null)
return;
// Manage only single click with left button
if ((event.getClickCount() != 1) || (event.getButton() != MouseEvent.BUTTON1))
return;
// Determine horizontal position of checkbox
BasicTreeUI ui;
int depth;
int leftIndent;
int rightIndent;
int checkboxLeft;
int checkboxRight;
ui = (BasicTreeUI)getUI();
depth = treePath.getPathCount();
leftIndent = ui.getLeftChildIndent();
rightIndent = ui.getRightChildIndent();
checkboxLeft = (depth - 1) * (leftIndent + rightIndent);
checkboxRight = checkboxLeft + checkboxWidth - 1;
// Ignore if not clicked on checkbox
int x;
x = event.getX();
if ((x < checkboxLeft) || (x > checkboxRight))
return;
// Toggle checkbox
toggleCheckBox(treePath);
} // mouseClicked
} // class TreeMouseManager
private class TreeKeyManager extends KeyAdapter
{
@Override
public void keyPressed(KeyEvent event)
{
// Determine selected element
TreePath treePath;
treePath = getSelectionPath();
if (treePath == null)
return;
// Manage event for this element
if (event.getKeyCode() == KeyEvent.VK_SPACE)
toggleCheckBox(treePath);
} // keyPressed
} // class TreeKeyManager
private class MyCellRenderer extends JPanel implements TreeCellRenderer
{
public MyCellRenderer()
{
// Create components
checkbox = new JCheckBox();
checkbox.setBorder(null);
checkbox.setOpaque(false);
label = new JLabel();
label.setBorder(new EmptyBorder(new Insets(0, 2, 0, 2)));
// Initialize panel
GridBagConstraints constraints;
setLayout(new GridBagLayout());
setOpaque(false);
constraints = new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
add(checkbox, constraints);
constraints = new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
add(label, constraints);
// Save the width of the checkbox
// We need it when the mouse is clicked on a node
checkboxWidth = (int)checkbox.getPreferredSize().getWidth();
} // constructor
@Override
public Component getTreeCellRendererComponent(JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus)
{
// Make data accessible
// Ignore if it's the root node
DefaultMutableTreeNode node;
NodeInfo info;
node = (DefaultMutableTreeNode)value;
if (node.getUserObject() instanceof NodeInfo)
info = (NodeInfo)node.getUserObject();
else
return (this);
// Determine font
Font font;
font = label.getFont();
if (info.checked)
font = font.deriveFont(font.getStyle() | Font.BOLD);
else
font = font.deriveFont(font.getStyle() & ~Font.BOLD);
// Configure components
checkbox.setSelected(info.checked);
label.setText(info.name);
label.setOpaque(selected);
label.setFont(font);
if (selected)
{
label.setBackground(SystemColor.textHighlight);
label.setForeground(SystemColor.textHighlightText);
}
else
{
label.setBackground(SystemColor.text);
label.setForeground(SystemColor.textText);
}
// Make sure everything is painted correctly
label.invalidate();
checkbox.invalidate();
invalidate();
// Done
return (this);
} // getTreeCellRendererComponent
private JCheckBox checkbox;
private JLabel label;
} // class MyCellRenderer
private static DefaultMutableTreeNode getNodes()
{
// Create root
DefaultMutableTreeNode root;
root = new DefaultMutableTreeNode("root");
// Create first level children
DefaultMutableTreeNode first;
DefaultMutableTreeNode second;
DefaultMutableTreeNode third;
NodeInfo info;
info = new NodeInfo();
info.name = "This is the first node";
info.checked = true;
first = new DefaultMutableTreeNode(info);
info = new NodeInfo();
info.name = "And this is the second";
info.checked = false;
second = new DefaultMutableTreeNode(info);
info = new NodeInfo();
info.name = "Finally, the third";
info.checked = false;
third = new DefaultMutableTreeNode(info);
root.add(first);
root.add(second);
root.add(third);
// Add second level children
info = new NodeInfo();
info.name = "Second level node";
info.checked = true;
first.add(new DefaultMutableTreeNode(info));
info = new NodeInfo();
info.name = "This is another one";
info.checked = false;
first.add(new DefaultMutableTreeNode(info));
info = new NodeInfo();
info.name = "And this is the last one";
info.checked = true;
first.add(new DefaultMutableTreeNode(info));
// Done
return (root);
} // getNodes
private static class NodeInfo
{
public String name;
public boolean checked;
}
} // class TestTree
更新
在 getTreeCellRendererComponent
内,我尝试获得首选尺寸。
他们看起来还不错。当 select 选中复选框时,标签和面板本身的首选尺寸都会增加。当取消select复选框时,它们会减少。
感谢这个问题Change JTree row height resizing behavior when rendering的回答,我自己解决了这个问题:
private void toggleCheckBox(TreePath treePath)
{
// Determine node being toggled
Object[] path;
DefaultMutableTreeNode node;
NodeInfo info;
path = treePath.getPath();
node = (DefaultMutableTreeNode)path[path.length - 1];
info = (NodeInfo)node.getUserObject();
// Toggle selection
info.checked = !info.checked;
// Make sure tree recalculates width of the nodes
BasicTreeUI ui = (BasicTreeUI)getUI();
try
{
Method method = BasicTreeUI.class.getDeclaredMethod("configureLayoutCache");
method.setAccessible(true);
method.invoke(ui);
}
catch (Exception e1)
{
e1.printStackTrace();
}
} // toggleCheckBox