我已经在https://github.com/graphstream/gs-ui-swing/issues/19#issue-2109865450中发布了我的问题。但由于此存储库上的最后一个答案已于 2021 年 6 月 10 日发布,并且其他问题已发布,但没有任何回复,我不确定是否仍然有人在关注那里的问题。这就是为什么我在这里重新问我的问题。
我创建了一个由一个View和两个JTextField组成的Jframe:
视图显示五个节点:四个只是地标,预计不会被用户移动(“fixed_*”),还有一个将被用户移动(“unfixed”)。两个 JTextField 显示“未固定”节点的坐标。 View 和 JTextField 都必须彼此同步。事实上,当用户移动视图中的“未固定”节点时,两个 JTextField 必须相应更新:
相反,当用户修改 JTextField 之一中的坐标时,视图也必须相应更新:
这里有四个测试用例:
测试用例 1、2 和 3 工作正常,但测试用例 4 不起作用。实际上,在测试用例 4 中,一旦用户移动了视图中的“未固定”节点,那么对其中一个 JTextField 中的坐标的修改不会更新视图。
我尝试分析测试用例 3 和 4 的执行之间有何不同。为此,我在代码的不同位置打印了当前线程的名称。我看到通过 JTextField 的修改是在线程“AWT-EventQueue-0”(Swing 的事件调度线程,不是吗?)上运行的,而通过 View 的修改是在线程“Thread-0”上运行的。在我的实现中,“Thread-0”是我运行 GraphStream 的泵送循环的线程,等待 GraphStream 的查看器线程中发生的事件并将它们复制回“Thread-0”内。根据我对 GraphStream 文档的理解:
我是否充分理解文档?
在我的实现中,我选择从 Swing 线程之外的另一个线程访问 GraphStream 的 Graph。因此,我从之前运行的测试用例 3 和 4 中推断出:
我的印象是我对所有这些线程都做错了。你能帮我一下吗?
我尝试制作一个最小工作示例(MWE)来重现我的问题。以下是 Java 源文件 NodeSyncTest.java 的内容:
package mwe;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import org.apache.commons.lang3.math.NumberUtils;
import org.graphstream.graph.Graph;
import org.graphstream.graph.Node;
import org.graphstream.graph.implementations.MultiGraph;
import org.graphstream.ui.graphicGraph.GraphicGraph;
import org.graphstream.ui.graphicGraph.GraphicNode;
import org.graphstream.ui.swing_viewer.SwingViewer;
import org.graphstream.ui.view.View;
import org.graphstream.ui.view.Viewer;
import org.graphstream.ui.view.ViewerListener;
import org.graphstream.ui.view.ViewerPipe;
class NodeSyncTest {
public static void main(String[] args) {
System.out.println("NodeSyncTest.main : " + Thread.currentThread().getName());
javax.swing.SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
System.out.println("SwingUtilities.invokeLater.Runnable.run : " + Thread.currentThread().getName());
new NodeSyncTest();
}
});
}
NodeSyncTest() {
Map<String, MyNode> myNodes = Map.of(
"fixed_top_left", new MyNode(-2, 2),
"fixed_top_right", new MyNode(2, 2),
"fixed_bottom_left", new MyNode(-2, -2),
"fixed_bottom_right", new MyNode(2, -2),
"unfixed", new MyNode(0, 0)
);
GraphStreamControl graphStreamControl = new GraphStreamControl(myNodes);
JTextFieldsControl jTextFieldsControl = new JTextFieldsControl(myNodes);
graphStreamControl.jTextFieldsControl = jTextFieldsControl;
jTextFieldsControl.graphStreamControl = graphStreamControl;
graphStreamControl.fillGraphStreamGraph();
JFrame mainDialog = new JFrame();
mainDialog.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainDialog.setSize(300, 300);
mainDialog.getContentPane().add((Component) graphStreamControl.view, BorderLayout.CENTER);
mainDialog.getContentPane().add(jTextFieldsControl.panel, BorderLayout.SOUTH);
mainDialog.setLocationRelativeTo(null);
mainDialog.setVisible(true);
graphStreamControl.startPumpLoop();
}
class GraphStreamControl {
Map<String, MyNode> myNodes;
MyNode myUnfixedNode;
Graph graphStreamGraph;
Viewer viewer;
View view;
ViewerPipe viewerPipe;
JTextFieldsControl jTextFieldsControl;
GraphStreamControl(Map<String, MyNode> myNodes) {
this.myNodes = myNodes;
myUnfixedNode = myNodes.get("unfixed");
System.setProperty("org.graphstream.ui", "swing");
graphStreamGraph = new MultiGraph("");
viewer = new SwingViewer(graphStreamGraph, Viewer.ThreadingModel.GRAPH_IN_ANOTHER_THREAD);
viewer.disableAutoLayout();
view = viewer.addDefaultView(false);
viewerPipe = viewer.newViewerPipe();
viewerPipe.addSink(graphStreamGraph);
viewerPipe.addViewerListener(new ViewerListener() {
@Override
public void viewClosed(String viewName) {}
@Override
public void buttonPushed(String id) {}
@Override
public void buttonReleased(String id) {
System.out.println("ViewerListener.buttonReleased : " + Thread.currentThread().getName());
if ("unfixed".equals(id)) {
GraphicGraph graphicGraph = viewer.getGraphicGraph();
GraphicNode unfixedGraphStreamNode = (GraphicNode) graphicGraph.getNode("unfixed");
myUnfixedNode.x = unfixedGraphStreamNode.getX();
myUnfixedNode.y = unfixedGraphStreamNode.getY();
jTextFieldsControl.update();
}
}
@Override
public void mouseOver(String id) {}
@Override
public void mouseLeft(String id) {}
});
}
public void fillGraphStreamGraph() {
for (var entry : myNodes.entrySet()) {
String nodeId = entry.getKey();
MyNode myNode = entry.getValue();
Node graphStreamNode = graphStreamGraph.addNode(nodeId);
graphStreamNode.setAttribute("xy", myNode.x, myNode.y);
graphStreamNode.setAttribute("ui.label", nodeId);
graphStreamNode.setAttribute("ui.style", "text-alignment: under;");
}
}
void startPumpLoop() {
new Thread(() -> {
System.out.println("GraphStreamControl.startPumpLoop : " + Thread.currentThread().getName());
while (true) {
try {
viewerPipe.blockingPump();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
void update() {
GraphicGraph graphicGraph = viewer.getGraphicGraph();
GraphicNode unfixedGraphStreamNode = (GraphicNode) graphicGraph.getNode("unfixed");
unfixedGraphStreamNode.setAttribute("xy", myUnfixedNode.x, myUnfixedNode.y);
}
}
class JTextFieldsControl {
Map<String, MyNode> myNodes;
MyNode myUnfixedNode;
JPanel panel;
JTextField xTextField;
JTextField yTextField;
GraphStreamControl graphStreamControl;
JTextFieldsControl(Map<String, MyNode> myNodes) {
this.myNodes = myNodes;
myUnfixedNode = myNodes.get("unfixed");
panel = new JPanel(new GridLayout(1, 4));
JLabel xLabel = new JLabel("X:", SwingConstants.RIGHT);
xLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panel.add(xLabel);
xTextField = new JTextField(3);
xTextField.setHorizontalAlignment(SwingConstants.RIGHT);
xTextField.setText(Double.toString(myUnfixedNode.x));
xTextField.getCaret().setDot(0);
xTextField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("JTextFieldsControl - actionPerformed on xTextField : " + Thread.currentThread().getName());
String xNodeString = xTextField.getText();
double xNodeDouble = NumberUtils.toDouble(xNodeString);
myUnfixedNode.x = xNodeDouble;
graphStreamControl.update();
}
});
panel.add(xTextField);
JLabel yLabel = new JLabel("Y:", SwingConstants.RIGHT);
yLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panel.add(yLabel);
yTextField = new JTextField(3);
yTextField.setHorizontalAlignment(SwingConstants.RIGHT);
yTextField.setText(Double.toString(myUnfixedNode.y));
yTextField.getCaret().setDot(0);
yTextField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("JTextFieldsControl - actionPerformed on yTextField : " + Thread.currentThread().getName());
String yNodeString = yTextField.getText();
double yNodeDouble = NumberUtils.toDouble(yNodeString);
myUnfixedNode.y = yNodeDouble;
graphStreamControl.update();
}
});
panel.add(yTextField);
}
void update() {
String xNodeString = Double.toString(myUnfixedNode.x);
xTextField.setText(xNodeString);
xTextField.getCaret().setDot(0);
String yNodeString = Double.toString(myUnfixedNode.y);
yTextField.setText(yNodeString);
yTextField.getCaret().setDot(0);
}
}
class MyNode {
double x;
double y;
MyNode(double x, double y) {
this.x = x;
this.y = y;
}
}
}
这是 Maven POM 文件 pom.xml 之一,用于构建包含所有依赖项的可执行 JAR:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>mwe</groupId>
<artifactId>mwe</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>MWE</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>org.graphstream</groupId>
<artifactId>gs-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.graphstream</groupId>
<artifactId>gs-ui-swing</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!-- Build an executable JAR -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<!-- here we specify that we want to use the main
method within the App class -->
<mainClass>mwe.NodeSyncTest</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>mwe.NodeSyncTest</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
</project>
要使用这两个文件,只需创建一个文件夹 mwe/,将 NodeSyncTest.java 放入 mwe/src/main/java/mwe/ 并将 pom.xml 放入 mwe/,然后在 mwe/ 中运行
mvn compile assembly:single
和
java -jar target/mwe-0.0.1-SNAPSHOT-jar-with-dependencies.jar
这是完整的项目文件夹:MWE.zip
经过一番调试,终于找到了问题出在哪里。我只需要更换线路:
unfixedGraphStreamNode.setAttribute("xy", myUnfixedNode.x, myUnfixedNode.y);
通过两行:
unfixedGraphStreamNode.setAttribute("x", myUnfixedNode.x);
unfixedGraphStreamNode.setAttribute("y", myUnfixedNode.y);
让一切顺利。
为什么
unfixedGraphStreamNode.setAttribute("xy", myUnfixedNode.x, myUnfixedNode.y);
不起作用对我来说仍然是个谜。事实上,https://graphstream-project.org/doc/Tutorials/Graph-Visualization/和https://graphstream-project.org/doc/FAQ/Attributes/Is-there-a-list的文档-of-attributes-with-a-predefined-meaning-for-the-graph-viewer/ 表示我们可以使用
xy
属性来设置节点的坐标。但也鼓励使用属性xyz
。因此,我尝试将代码更改为:
unfixedGraphStreamNode.setAttribute("xyz", myUnfixedNode.x, myUnfixedNode.y, 0.0);
而且它有效!我将在项目的 GitHub 存储库上发布问题。