我要做的是实现一个自定义控件来编辑列表属性(1:n关系)。由于我没有足够的声誉来发布截图,我将尽我所能来描述它。它包含一个记录列表作为值,每个记录由一个RecordPanel表示,它基本上是一个记录的编辑表单。
控制菜单提供添加,删除和复制记录的可能性,以及浏览RecordPanels。仅显示活动的RecordPanel。在活动RecordPanel的左侧,使用我的自定义TreeTableModel(这是我的问题的原因)的JXTreeTable用于显示控件中包含的记录列表。值记录构成模型的叶子,父项可以从相应子项的属性派生。该树可用于直接跳转到特定记录。
为了构建TreeTableModel,我使用XML模板。例如:
<?xml version="1.0" encoding="UTF-8"?>
<treetable prefix="Drug" columns="name">
<branch entity="JpaDrugCharacterization" children="drugs" displayFields="name"/>
<branch entity="JpaDrug" parent="drugCharacterization" displayFields="name"/>
</treetable>
模型使用TreeTableNodes,它包含实际记录,以及它们的父节点和子节点。因此,可以从节点构建完整的树结构。
XML模板中包含的分支定义将用于创建TreeTableBranch对象,这些对象知道如何检索分支父项和子项以及要在树表中显示的字段。
以下是用于构建NodeTreeTableModel的关键方法。我没有包含AbstractTreeTableModel的重写方法。
/**
* Builds a tree table model based on {@link TreeTableNode}s and {@link TreeTableBranch}es.
* Can be initialized using an XML template or by using the root node of a TreeTableNode structure
* together with a branch definition created by other means (e.g. programmatically).
*
* @author Lellebebbel
*
*/
public class NodeTreeTableModel extends AbstractTreeTableModel implements DebugLogger {
/**
* The root node of the tree table serving as the master parent.
* Entry point for {@link JXTreeTable} to build the tree. Not shown in GUI.
*/
private TreeTableNode rootNode;
/**
* Contains a hashtable mapping each branch definition
* to the class name of the corresponding class in the tree.
*/
private Hashtable<String, TreeTableBranch> branches = new Hashtable<>();
/**
* List of the column names given in the template.
* Used for initialization of the {@link fieldMapPool}
* and creation of the {@link JXTreeTable}.
*/
private List<String> columnNames = new ArrayList<>();
/**
* Contains a hashtable mapping the id of the contained objects to the respective TreeTableNode.
* Used to prevent duplicate creation of TreeTableNodes containing the same object.
*/
private Hashtable<Integer, TreeTableNode> nodeList = new Hashtable<>();
/**
* Contains a hashtable mapping the child class name to the corresponding dummy parent.
*/
private Hashtable<String, NamedPositionable> dummyParents = new Hashtable<>();
NodeTreeTableModel(TreeTableNode rootNode, Document template, List<? extends NamedPositionable> valueList) {
super(rootNode);
this.rootNode = rootNode;
this.initialize(template);
this.createTreeStructure(valueList);
}
/**
* Create a tree table structure based on the values set for the tree table.
*
* @param valueList the values of the tree table
*/
private void createTreeStructure(List<? extends NamedPositionable> valueList) {
// browse through the valueList (containing the designated leaves for the tree)
for (NamedPositionable leaf : valueList) {
// create the tree table node for the leaf
TreeTableNode leafNode = new TreeTableNode(leaf);
// link all parents up to the tree rootNode to the leaf
TreeTableNode linkNode = leafNode;
while (!linkNode.equals(this.rootNode)) {
linkNode = this.linkToParent(linkNode);
}
nodeList.put(leaf.getId(), leafNode);
}
}
/**
* Evaluate the template, initialize translation prefix, column names and branch definitions.
*
* @param template The template to be evaluated
*/
private void initialize(Document template) {
// start parsing of template
Element root = template.getDocumentElement();
// initialize the translation prefix
this.prefix = XMLTools.getAttribute(root, "prefix", true);
// initialize the column names
String[] columnNameArray = XMLTools.getAttribute(root, "columns", true).split(",");
for (String columnName : columnNameArray) {
this.columnNames.add(Translator.get(this.prefix + "." + columnName));
}
// initialize the branches
NodeList branchNodes = root.getElementsByTagName("branch");
for (int i = 0; i < branchNodes.getLength(); i++) {
Node branchNode = branchNodes.item(i);
// retrieve the class name to be used as key
String className = XMLTools.getAttribute(branchNode, "entity", true);
// create the tree table branch to be used as value
TreeTableBranch branch = new TreeTableBranch();
String[] displayFields = XMLTools.getAttribute(branchNode, "displayFields", true).split(",");
for (int j = 0; j < this.columnNames.size(); j++) {
branch.addDisplayField(columnNames.get(j), displayFields[j]);
}
String parentField = XMLTools.getAttribute(branchNode, "parent", false);
branch.setParentField(parentField);
String childField = XMLTools.getAttribute(branchNode, "children", false);
branch.setChildField(childField);
this.branches.put(className, branch);
}
}
/**
* Links a given node to its parentNode an vice versa.
* Part of the tree table structure building routine.
*
* @param linkNode the childNode to be linked
* @return parentNode the linked parentNode
*/
private TreeTableNode linkToParent(TreeTableNode linkNode) {
// find the branch of the linkNode
TreeTableBranch branch = branches.get(linkNode.getContainedClassName());
// if branch does not specify a parent, use rootNode
if (branch.getParentField() == null) {
linkNode.setParent(this.rootNode);
this.rootNode.addChild(linkNode);
return this.rootNode;
}
// get the parent object
NamedPositionable parent =
(NamedPositionable) ReflectionTools.getValue(linkNode.getContainedObject(), branch.getParentField());
if (parent == null) {
parent = this.getDummyParent(linkNode.getContainedClassName());
}
TreeTableNode parentNode;
// if parent node has already been created
if (this.nodeList.containsKey(parent.getId())) {
// take it
parentNode = this.nodeList.get(parent.getId());
// establish the link
linkNode.setParent(parentNode);
parentNode.addChild(linkNode);
// return root node, as the parent is already connected to root
return this.rootNode;
} else {
// create it
parentNode = new TreeTableNode(parent);
// establish the link
linkNode.setParent(parentNode);
parentNode.addChild(linkNode);
// put the new parent in the nodeList and return it
this.nodeList.put(parent.getId(), parentNode);
return parentNode;
}
}
/**
* Returns the proper dummy parent for a given class name of a child.
* If the dummy parent does not exist, it will be created.
*
* @param childClassName name of the child class
* @return the dummy parent
*/
@SuppressWarnings("unchecked")
private NamedPositionable getDummyParent(String childClassName) {
NamedPositionable dummyParent = this.dummyParents.get(childClassName);
if (dummyParent != null) {
return dummyParent;
}
/*
* As we do not have a fitting dummy parent, we have to create it...
* We start by finding out, of which class the parent should be.
*/
TreeTableBranch childBranch = this.branches.get(childClassName);
/*
* As IdRecord should be the most common case for tree children,
* the short version of the class name may be used in the template.
*/
Class<? extends NamedPositionable> childClass;
if (!childClassName.contains(".")) {
childClass = ReflectionTools.getClassByEntityName(childClassName);
} else {
childClass = (Class<? extends NamedPositionable>) ReflectionTools.getClassByName(childClassName);
}
String methodName = ReflectionTools.getGetterName(childBranch.getParentField());
try {
Class<? extends NamedPositionable> parentClass = (Class<? extends NamedPositionable>) childClass
.getMethod(methodName, (Class<?>[]) null).getReturnType();
// as we know the class now, we can create the parent object
dummyParent = parentClass.newInstance();
// now we need to set the initial values of the parent object
dummyParent.initializeId();
TreeTableBranch parentBranch = this.branches.get(parentClass.getSimpleName());
for (String fieldName : parentBranch.getDisplayFields()) {
ReflectionTools.setValue(dummyParent, fieldName, Translator.get("NodeTreeTableModel.undefinedNode"));
}
// finally, we can add the parent to the dummy parent list and return it
this.dummyParents.put(childClassName, dummyParent);
return dummyParent;
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException e) {
Monitor.capture(e);
return null;
}
}
}
最后,以下是用于绘制控件和更新treetable的方法。
/**
* Rebuilds the tree table. Should be called, when value has changed.
*/
public void buildTreeTable() {
this.treeTable = new RecordPaginationTreeTable(this.createTreeTableModel());
this.treeTable.setAutoResizeMode(JXTreeTable.AUTO_RESIZE_ALL_COLUMNS);
// Calculate max width of the tree table
this.treeTable.expandAll();
Dimension treeTableSize = this.treeTable.getPreferredSize();
Integer totalColumnWidth = TableTools.getCalculatedTableWidth(treeTable);
this.treeTable.setPreferredSize(new Dimension(totalColumnWidth, treeTableSize.height));
// Notify the tree updaters that the table has been rebuild
this.pagination.notifyTreeUpdaters();
NodeTreeTableModel nttm = (NodeTreeTableModel) this.getTreeTable().getTreeTableModel();
nttm.dumpTreeStructure();
}
/*
* (non-Javadoc)
*
* @see de.stada.pvapp.client.gui.interfaces.FormComponent#drawComponent()
*/
@Override
public void drawComponent() {
// Basic UI settings
this.removeAll();
this.setOpaque(false);
this.buildTreeTable();
// Set selection mode to single selection and opacity
this.treeTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
this.treeTable.setOpaque(false);
this.treeTable.setTableHeader(null);
Color alphaColor = new Color(0, 0, 0, 0);
treeTable.setBackground(alphaColor);
// Build pagination
this.pagination = new RecordPagination();
for (IdRecord record : this.value) {
RecordPanel rp = module.createRecordPanel(record, this.editMode);
rp.setOpaque(false);
pagination.addPanel(rp);
}
// Set the layout as follows
//
// ___________________________________
// | _____________| |
// | | | |
// | | | |
// | | | |
// | | tree | pagination |
// | | | |
// | | | |
// | |_____________| |
// |_______________|___________________|
//
String layout;
String[] layoutParams = this.getParameters("layout");
if (layoutParams != null && layoutParams.length > 0) {
layout = layoutParams[0];
} else {
layout = DEFAULT_LAYOUT;
}
this.setLayout(new FormLayout(LayoutConfig.getLayout(layout), "$vBorder,fill:d:grow,$vBorder"));
// Build split pane
this.add(this.treeTable, new CellConstraints(2, 2));
this.add(this.pagination, new CellConstraints(3, 1, 1, 3));
// add listeners
this.addDefaultListeners();
}
只要我从数据库中获得给定值,一切都按预期工作。正确构建模型,控件和GUI。但是当我试图向控件添加新记录时,一个新因素开始起作用:
如上所述,树中叶子的父亲来自叶子的属性。在上面的XML模板中给出的示例中,药物的父母将是药物特征化。然而,当我创造一种新药时,药物特征当然不为人所知。为了在树中显示它,我为这些新记录创建了虚拟父项(标记为-undefined-)。只要在编辑过程中定义了drugCharacterization,就会更新树,并将记录放在正确的父级下。要创建dummyParents,请在NodeTreeTableModel中查看方法linkToParent和getDummyParent。
现在我的问题:据我所知,dummyParents在创建新记录时被正确创建和链接,新记录被添加到值中,并且可以通过浏览面板来访问相应的RecordPanel。但是,它们实际上从未在GUI中的树中显示。
应该有类似的东西:
-undefined-
new drug
但它不存在。我完全不知道为什么会这样。
2015九月08: 如果我从数据库加载一个带有未定义父对象的对象,它将在GUI中正确显示。如果我在运行时添加一个,则不是。
我在树表节点上注册了几个关键侦听器,如果节点名称发生更改,则会更新树。这适用于现有节点。但是,在由关键侦听器触发的重新绘制后,新的未定义节点不会显示。
问题似乎位于buildTreeTable方法中。我在代码中看到了一个新的树形表,其中包含一个新的treetablemodel。但显然旧结构用于这个新表。
如何使用新模型的结构使TreeTable正确重绘。我试图使(),repaint(),updateUI()无效。一切都徒劳无功。