Изменить поведение изменения размера строки JTree при рендеринге
Я хочу использовать пользовательский TreeCellRenderer, содержащий три текстовых поля, только когда узел ВЫБРАН, в то время как использовать средство визуализации по умолчанию, когда узел НЕ ВЫБРАН. Проблема в том, что, хотя я установил подходящий предпочтительный и минимальный размер для панели, JTree не обновляет отредактированную высоту строки. Напротив, когда я использую ту же панель, что и редактор, она отображается правильно.
Может кто-нибудь объяснить, почему это происходит?
Есть ли рекомендуемый способ добиться изменения размера рендеринга, похожего на редактирование?
Есть ли метод, предоставляемый JTree, чтобы установить его напрямую или необходимо расширить JTree или (что еще хуже) L&F?
ПРИМЕЧАНИЕ: после копания в BasicTreeUI.startEditing(TreePath path, MouseEvent event)
Метод, я заметил следующие строки кода. Похоже, они отвечают за изменение размера:
if(editorSize.width != nodeBounds.width ||
editorSize.height != nodeBounds.height) {
// Editor wants different width or height, invalidate
// treeState and relayout.
editorHasDifferentSize = true;
treeState.invalidatePathBounds(path);
updateSize();
// To make sure x/y are updated correctly, fetch
// the bounds again.
nodeBounds = getPathBounds(tree, path);
}
else
editorHasDifferentSize = false;
tree.add(editingComponent);
editingComponent.setBounds(nodeBounds.x, nodeBounds.y,
nodeBounds.width,
nodeBounds.height);
Вот SSCCE, показывающий различное поведение редактирования и рендеринга.
- Когда узел не выбран, используется средство визуализации по умолчанию.
- Щелкнув один раз на узле, узел выбирается и используется средство визуализации панели.
- При двойном щелчке по узлу начинается редактирование и используется редактор панели.
Как вы увидите, панель отображается правильно только во время редактирования.
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.MouseEvent;
import java.util.EventObject;
import javax.swing.AbstractCellEditor;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
public class TestResizeTreeRowsFrame {
public static void main(String[] args){
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
MyTreeNode root = createRoot();
TestFrame f = new TestFrame(root);
f.setVisible(true);
}
});
}
private static MyTreeNode createRoot(){
MyTreeNode root = new MyTreeNode("RootColumnName", "RootTableName", "Root");
MyTreeNode aNode = new MyTreeNode("AcolumnName", "AtableName", "A");
MyTreeNode bNode = new MyTreeNode("BcolumnName", "BtableName", "B");
MyTreeNode cNode = new MyTreeNode("CcolumnName", "CtableName", "C");
root.add(aNode);
root.add(bNode);
root.add(cNode);
return root;
}
public static class MyTreeNode extends DefaultMutableTreeNode{
/**
*
*/
private static final long serialVersionUID = 1L;
String columnName, tableName, value;
public MyTreeNode(String columnName, String tableName, String value){
this.columnName = columnName;
this.tableName = tableName;
this.value = value;
}
public String getColumnName() {
return columnName;
}
public String getTableName() {
return tableName;
}
public String getValue() {
return value;
}
public String toString(){
return value;
}
}
public static class TestFrame extends JFrame {
/**
*
*/
private static final long serialVersionUID = 1L;
private JPanel contentPane;
private JTree tree;
/**
* Create the frame.
*/
public TestFrame(MyTreeNode root) {
this.setTitle("RECORD Frame");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 450, 300);
contentPane = new JPanel();
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
contentPane.setLayout(new BorderLayout(0, 0));
setContentPane(contentPane);
JScrollPane scrollPane = new JScrollPane();
contentPane.add(scrollPane, BorderLayout.CENTER);
tree = new JTree(root);
scrollPane.setViewportView(tree);
tree.setEditable(true);
tree.setInvokesStopCellEditing(true);
tree.setCellRenderer(new NodeRenderer());
tree.setCellEditor(new PanelRenderer());
}
private static class Renderer_Panel extends JPanel{
/**
*
*/
private static final long serialVersionUID = 1L;
private JTextField propertyTextField;
private JTextField prototypeTextField;
private JTextField valueTextField;
/**
* Create the panel.
*/
public Renderer_Panel() {
setPreferredSize(new Dimension(480, 97));
setMinimumSize(new Dimension(480, 97));
setLayout(new BorderLayout(0, 0));
JPanel panel = new JPanel();
panel.setMinimumSize(new Dimension(480, 97));
panel.setPreferredSize(new Dimension(480, 97));
add(panel, BorderLayout.CENTER);
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
Component verticalGlue_1 = Box.createVerticalGlue();
panel.add(verticalGlue_1);
JScrollPane scrollPane = new JScrollPane();
scrollPane.setBorder(null);
scrollPane.setPreferredSize(new Dimension(20, 60));
JPanel nodePropertiesPanel = new JPanel();
nodePropertiesPanel.setBorder(new EmptyBorder(0, 3, 0, 0));
nodePropertiesPanel.setPreferredSize(new Dimension(200, 30));
nodePropertiesPanel.setMinimumSize(new Dimension(0, 0));
scrollPane.setViewportView(nodePropertiesPanel);
GridBagLayout gbl_panel = new GridBagLayout();
gbl_panel.columnWidths = new int[]{0, 0, 0};
gbl_panel.rowHeights = new int[]{0, 0, 0, 0};
gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
gbl_panel.rowWeights = new double[]{0.0, 0.0, 0.0, Double.MIN_VALUE};
nodePropertiesPanel.setLayout(gbl_panel);
JLabel lblProperty = new JLabel("Column:");
GridBagConstraints gbc_lblProperty = new GridBagConstraints();
gbc_lblProperty.insets = new Insets(0, 0, 5, 5);
gbc_lblProperty.anchor = GridBagConstraints.WEST;
gbc_lblProperty.gridx = 0;
gbc_lblProperty.gridy = 0;
nodePropertiesPanel.add(lblProperty, gbc_lblProperty);
propertyTextField = new JTextField();
GridBagConstraints gbc_propertyTextField = new GridBagConstraints();
gbc_propertyTextField.insets = new Insets(0, 0, 5, 0);
gbc_propertyTextField.fill = GridBagConstraints.HORIZONTAL;
gbc_propertyTextField.gridx = 1;
gbc_propertyTextField.gridy = 0;
nodePropertiesPanel.add(propertyTextField, gbc_propertyTextField);
propertyTextField.setColumns(10);
JLabel lblPrototype = new JLabel("Table:");
GridBagConstraints gbc_lblPrototype = new GridBagConstraints();
gbc_lblPrototype.anchor = GridBagConstraints.WEST;
gbc_lblPrototype.insets = new Insets(0, 0, 5, 5);
gbc_lblPrototype.gridx = 0;
gbc_lblPrototype.gridy = 1;
nodePropertiesPanel.add(lblPrototype, gbc_lblPrototype);
prototypeTextField = new JTextField();
GridBagConstraints gbc_prototypeTextField = new GridBagConstraints();
gbc_prototypeTextField.insets = new Insets(0, 0, 5, 0);
gbc_prototypeTextField.fill = GridBagConstraints.HORIZONTAL;
gbc_prototypeTextField.gridx = 1;
gbc_prototypeTextField.gridy = 1;
nodePropertiesPanel.add(prototypeTextField, gbc_prototypeTextField);
prototypeTextField.setColumns(10);
JLabel lblNewLabel = new JLabel("Value:");
GridBagConstraints gbc_lblNewLabel = new GridBagConstraints();
gbc_lblNewLabel.anchor = GridBagConstraints.WEST;
gbc_lblNewLabel.insets = new Insets(0, 0, 0, 5);
gbc_lblNewLabel.gridx = 0;
gbc_lblNewLabel.gridy = 2;
nodePropertiesPanel.add(lblNewLabel, gbc_lblNewLabel);
valueTextField = new JTextField();
GridBagConstraints gbc_valueTextField = new GridBagConstraints();
gbc_valueTextField.fill = GridBagConstraints.HORIZONTAL;
gbc_valueTextField.gridx = 1;
gbc_valueTextField.gridy = 2;
nodePropertiesPanel.add(valueTextField, gbc_valueTextField);
valueTextField.setColumns(10);
panel.add(scrollPane);
Component verticalGlue = Box.createVerticalGlue();
panel.add(verticalGlue);
}
public void setProperty(String property){
this.propertyTextField.setText(property);
}
public void setPrototype(String prototype){
this.prototypeTextField.setText(prototype);
}
public void setValue(String value){
this.valueTextField.setText(value);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(480, 97);
}
@Override
public Dimension getMinimumSize() {
return new Dimension(480, 97);
}
}
private class PanelRenderer extends AbstractCellEditor implements TreeCellEditor, TreeCellRenderer{
/**
*
*/
private static final long serialVersionUID = 1L;
Renderer_Panel component = new Renderer_Panel();
MyTreeNode value;
@Override
public Component getTreeCellEditorComponent(JTree tree,
Object value, boolean isSelected, boolean expanded,
boolean leaf, int row) {
MyTreeNode myNode = ((MyTreeNode)value);
String nodeValue = null;
String prototype = null;
String property = null;
nodeValue = myNode.getValue();
prototype = myNode.getTableName();
property = myNode.getColumnName();
component.setProperty(property);
component.setPrototype(prototype);
component.setValue(nodeValue);
this.value = myNode;
return component;
}
@Override
public Object getCellEditorValue() {
return this.value.getValue();
}
@Override
public boolean isCellEditable(EventObject anEvent) {
if(anEvent instanceof MouseEvent){
MouseEvent mouseEvent = (MouseEvent)anEvent;
if(mouseEvent.getClickCount() == 2){
return true;
}else{
return false;
}
}else{
return false;
}
}
@Override
public Component getTreeCellRendererComponent(JTree tree,
Object value, boolean selected, boolean expanded,
boolean leaf, int row, boolean hasFocus) {
return getTreeCellEditorComponent(tree, value, selected, expanded, leaf, row);
}
}
private class LabelNodeRenderer extends DefaultTreeCellRenderer {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
MyTreeNode myNode = ((MyTreeNode)value);
this.setText(myNode.getValue());
return this;
}
}
private class NodeRenderer implements TreeCellRenderer{
/**
*
*/
private static final long serialVersionUID = 1L;
private LabelNodeRenderer labelRenderer = new LabelNodeRenderer();
private PanelRenderer panelRenderer = new PanelRenderer();
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
Component returnedComponent = null;
if(selected){
returnedComponent = panelRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
}else{
returnedComponent = labelRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
}
returnedComponent.setSize(returnedComponent.getPreferredSize());
return returnedComponent;
}
}
}
}
Кроме того, прости меня, если это не то место, но я пользуюсь возможностью спросить, есть ли хорошая книга, которая рекомендует лучшие практики для Swing?
Разве архитекторы Swing где-то документировали рекомендуемые решения, на которых они основывали свой дизайн Swing?(Я знаю, я слишком много спрашиваю)
Есть ли хотя бы кулинарная книга, в которой есть совет, такой как комментарий Клеопатры, найденный в JTree TreeCellRenderer, поднимающий проблему с отображением цвета выделения:
a) extending a component is dirty design b) mixing calls to super and this is calling for pain (f.i. the infamous color memory in the default table cell renderer)
или объясните конструктивные решения, такие как заставить CellEditorListener прослушивать только для editCanceled и editStopped, но не для editStarted (что было бы полезно в случае, если я хочу изменить размеры ячеек JTable без необходимости переопределять JTable.editCellAt).
Заранее спасибо!
1 ответ
Некоторые факты:
- BasicTreeUI хранит кеш размеров узлов
- нет публичного API, чтобы заставить его повторно проверить этот кеш
- Предполагается, что требования к размеру узла зависят исключительно от данных, а не от визуального состояния, то есть изменение состояния выбора не приведет к какому-либо внутреннему обновлению.
- в сторону: установка размера внутри рендерера / редактора не имеет никакого эффекта: что бы вы ни делали, пользовательский интерфейс изменит его, если сочтет нужным
В общем, нет способа выполнить ваше требование, не испачкавшись. По сути, вы должны слушать изменения выбора - потому что у рендерера есть разные требования к размеру в выбранных и не выбранных - и затем сделать все возможное, чтобы аннулировать внутренний кеш пользовательского интерфейса. В основном два варианта:
- использовать отражение для доступа к защищенным методам пользовательского интерфейса
- Поддельные события модели, которые привели бы к внутреннему перерасчету кеша
Ниже приведен фрагмент для первого (не смог сделать вторую работу по быстрому тесту, но слабо помню, что я сделал это...)
protected TreeSelectionListener createReflectiveSelectionListener() {
TreeSelectionListener l = new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
invalidateLayoutCache();
}
protected void invalidateLayoutCache() {
BasicTreeUI ui = (BasicTreeUI) tree.getUI();
try {
Method method = BasicTreeUI.class.getDeclaredMethod("configureLayoutCache");
method.setAccessible(true);
method.invoke(ui);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e1) {
e1.printStackTrace();
}
}
};
return l;
}
Просто нашел второй - такой же уровень загрязнения, как и первый - вариант:
protected TreeSelectionListener createFakeDataEventSelectionListener() {
TreeSelectionListener l = new TreeSelectionListener() {
@Override
public void valueChanged(final TreeSelectionEvent e) {
fireDataChanged(e.getOldLeadSelectionPath());
fireDataChanged(e.getNewLeadSelectionPath());
}
private void fireDataChanged(TreePath lead) {
if (lead == null) return;
DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
TreeNode last = (TreeNode) lead.getLastPathComponent();
model.nodeChanged(last);
}
};
return l;
}