我要做的是实现一个自定义控件来编辑列表属性(1:n 关系)。由于我没有足够的声誉来发布截图,我会尽力描述它。它包含一个记录列表作为值,每个记录由一个 RecordPanel 表示,它基本上是记录的编辑表单。
控制菜单提供了添加、删除和复制记录以及浏览记录面板的可能性。仅显示活动的 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>
该模型使用包含实际记录及其父节点和子节点的 TreeTableNode。因此,可以从节点构建完整的树结构。
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;
}
}
}
最后,这里是用于绘制控件和更新树表的方法。
/**
* 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 模板中给出的示例中,药物的父级是 drugCharacterization。但是,当我创建一种新药时,drugCharacterization 当然是未知的。为了在树中显示它,我为这些新记录创建了虚拟父母(标记为 -undefined-)。一旦在编辑过程中定义了 drugCharacterization,就会更新树并将记录放置在适当的父级下。要创建 dummyParent,请参见 NodeTreeTableModel 中的方法 linkToParent 和 getDummyParent。
现在我的问题是:据我所知,dummyParents 在创建新记录时被创建并正确链接,新记录被添加到值中,并且可以通过浏览面板访问相应的 RecordPanel。但是,它们从未真正显示在 GUI 的树中。
应该有类似的东西:
-undefined-
new drug
但它只是不存在。我完全不知道,为什么会这样。
2015 年 9 月 8 日:如果我从数据库加载一个未定义父对象的对象,它会在 GUI 中正确显示。如果我在运行时添加一个,则不是。
我在树表节点上注册了几个关键侦听器,如果节点名称发生更改,它们会更新树。这适用于现有节点。尽管如此,在关键侦听器触发的那些重绘之后,新的未定义节点不会显示。
问题似乎出在 buildTreeTable 方法中。正如您在代码中看到的那样,我使用新的 treetablemodel 构建了一个新的树表。但显然旧结构用于这个新表。
如何让 TreeTable 使用新模型的结构正确重绘。我试图使其无效(),重绘(),更新UI()。都是徒劳的。