如何在JFrame中使用多JPanel实现K-Means聚类可视化
嘿,我来帮你解决这个Swing可视化K-Means的问题!你遇到的第二个面板不响应的问题,还有想把ClusterPanel和NodePanel做成独立类、实现动态更新的需求,咱们一步步来搞定。
一、核心问题分析:重叠面板的层级与事件冲突
你当前的代码里把canvas、clusterPanel、nodePanel都放在了同一个坐标位置(10,10,480,480),且使用了null布局。Swing中后添加的组件会处于视觉层级的最上层,所以最后添加的nodePanel会完全覆盖clusterPanel,导致下层的clusterPanel无法接收鼠标事件。
同时代码里还存在变量名冲突(比如isPaintNode既是类名又是布尔变量名)、单选按钮未互斥等小问题,这些都会影响功能正常运行。
二、重构方案:独立面板类+动态更新机制
咱们把ClusterPanel和NodePanel抽成独立的JPanel子类,各自维护数据和绘制逻辑,再通过主窗口协调它们的交互与动态更新。
1. 独立的ClusterPanel类
负责聚类中心的存储、绘制与位置更新:
import javax.swing.*; import java.awt.*; import java.util.ArrayList; import java.util.List; public class ClusterPanel extends JPanel { private List<Cluster> clusters = new ArrayList<>(); public ClusterPanel() { setOpaque(false); // 设置透明,避免遮挡下层节点面板 setLayout(null); } // 添加新聚类中心 public void addCluster(Cluster cluster) { clusters.add(cluster); repaint(); } // 更新聚类中心位置 public void updateClusterPosition(int index, Point newPos) { if (index >= 0 && index < clusters.size()) { clusters.get(index).setPos(newPos); repaint(); } } public List<Cluster> getClusters() { return clusters; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 绘制聚类中心(大圆圈+编号) for (Cluster cluster : clusters) { g2d.setColor(cluster.getColor()); g2d.fillOval(cluster.getPos().x - 15, cluster.getPos().y - 15, 30, 30); g2d.setColor(Color.BLACK); g2d.drawString(String.valueOf(cluster.getId()), cluster.getPos().x - 5, cluster.getPos().y + 5); } } // 聚类数据结构 public static class Cluster { private int id; private Point pos; private Color color; public Cluster(int id, Point pos, Color color) { this.id = id; this.pos = pos; this.color = color; } // Getter & Setter public int getId() { return id; } public Point getPos() { return pos; } public void setPos(Point pos) { this.pos = pos; } public Color getColor() { return color; } } }
2. 独立的NodePanel类
负责节点的存储、绘制与聚类颜色更新:
import javax.swing.*; import java.awt.*; import java.util.ArrayList; import java.util.List; public class NodePanel extends JPanel { private List<Node> nodes = new ArrayList<>(); public NodePanel() { setOpaque(false); setLayout(null); } // 添加新节点 public void addNode(Node node) { nodes.add(node); repaint(); } // 批量更新所有节点的聚类颜色 public void updateAllNodeClusterColors(List<Integer> clusterAssignments, List<ClusterPanel.Cluster> clusters) { for (int i = 0; i < nodes.size(); i++) { int clusterId = clusterAssignments.get(i); if (clusterId >= 0 && clusterId < clusters.size()) { nodes.get(i).setClusterColor(clusters.get(clusterId).getColor()); } } repaint(); } public List<Node> getNodes() { return nodes; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 绘制节点(小圆圈+黑色边框) for (Node node : nodes) { g2d.setColor(node.getClusterColor()); g2d.fillOval(node.getPos().x - 8, node.getPos().y - 8, 16, 16); g2d.setColor(Color.BLACK); g2d.drawOval(node.getPos().x - 8, node.getPos().y - 8, 16, 16); } } // 节点数据结构 public static class Node { private Point pos; private Color clusterColor; public Node(Point pos) { this.pos = pos; this.clusterColor = Color.GRAY; // 默认灰色(未分配聚类) } // Getter & Setter public Point getPos() { return pos; } public Color getClusterColor() { return clusterColor; } public void setClusterColor(Color clusterColor) { this.clusterColor = clusterColor; } } }
3. 重构后的MainUI主窗口
修复布局、事件逻辑,添加K-Means迭代功能:
import javax.swing.*; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; import java.util.Random; public class MainUI extends JFrame { private JPanel drawContainer; private NodePanel nodePanel; private ClusterPanel clusterPanel; private boolean paintNodeMode = false; private boolean paintClusterMode = false; private int clusterIdCounter = 0; private Random random = new Random(); public MainUI(String title) { super(title); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); init(); setSize(700, 540); centerWindow(); setVisible(true); } private void init() { setLayout(null); // 创建绘制容器,作为节点和聚类面板的父容器 drawContainer = new JPanel(); drawContainer.setBackground(Color.WHITE); drawContainer.setBounds(10, 10, 480, 480); drawContainer.setLayout(null); add(drawContainer); // 初始化两个独立面板 nodePanel = new NodePanel(); clusterPanel = new ClusterPanel(); nodePanel.setBounds(0, 0, 480, 480); clusterPanel.setBounds(0, 0, 480, 480); // 节点面板点击事件:添加节点 nodePanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (paintNodeMode) { NodePanel.Node node = new NodePanel.Node(e.getPoint()); nodePanel.addNode(node); } } }); // 聚类面板点击事件:添加聚类中心 clusterPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (paintClusterMode) { Color randomColor = new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)); ClusterPanel.Cluster cluster = new ClusterPanel.Cluster(clusterIdCounter++, e.getPoint(), randomColor); clusterPanel.addCluster(cluster); } } }); // 先加节点面板,再加聚类面板,确保聚类显示在节点上层 drawContainer.add(nodePanel); drawContainer.add(clusterPanel); // 右侧按钮面板 JPanel buttonPanel = new JPanel(); buttonPanel.setBounds(500, 10, 180, 480); buttonPanel.setLayout(null); add(buttonPanel); // 单选按钮组(互斥选择模式) ButtonGroup modeGroup = new ButtonGroup(); JRadioBtn radioNode = new JRadioButton("添加节点"); radioNode.setBounds(20, 200, 120, 20); radioNode.addActionListener(e -> paintNodeMode = radioNode.isSelected()); modeGroup.add(radioNode); JRadioBtn radioCluster = new JRadioButton("添加聚类"); radioCluster.setBounds(20, 230, 120, 20); radioCluster.addActionListener(e -> paintClusterMode = radioCluster.isSelected()); modeGroup.add(radioCluster); buttonPanel.add(radioNode); buttonPanel.add(radioCluster); // K-Means迭代按钮 JButton iterateBtn = new JButton("执行K-Means迭代"); iterateBtn.setBounds(20, 260, 140, 30); iterateBtn.addActionListener(e -> runKMeansIteration()); buttonPanel.add(iterateBtn); } // 窗口居中 private void centerWindow() { Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); Dimension frameSize = getSize(); int x = (screen.width - frameSize.width) / 2; int y = (screen.height - frameSize.height) / 2; setLocation(x, y); } // 模拟K-Means迭代步骤 private void runKMeansIteration() { List<NodePanel.Node> nodes = nodePanel.getNodes(); List<ClusterPanel.Cluster> clusters = clusterPanel.getClusters(); if (nodes.isEmpty() || clusters.isEmpty()) { JOptionPane.showMessageDialog(this, "请先添加节点和聚类中心!"); return; } // 1. 分配每个节点到最近的聚类 List<Integer> assignments = new ArrayList<>(); for (NodePanel.Node node : nodes) { assignments.add(findClosestCluster(node.getPos(), clusters)); } // 2. 更新节点颜色 nodePanel.updateAllNodeClusterColors(assignments, clusters); // 3. 更新聚类中心位置 updateClusterCenters(assignments, nodes, clusters); } // 找到最近的聚类中心 private int findClosestCluster(Point nodePos, List<ClusterPanel.Cluster> clusters) { double minDistance = Double.MAX_VALUE; int closestId = 0; for (int i = 0; i < clusters.size(); i++) { double distance = calculateDistance(nodePos, clusters.get(i).getPos()); if (distance < minDistance) { minDistance = distance; closestId = i; } } return closestId; } // 计算两点欧氏距离 private double calculateDistance(Point p1, Point p2) { int dx = p1.x - p2.x; int dy = p1.y - p2.y; return Math.sqrt(dx*dx + dy*dy); } // 更新聚类中心到所属节点的均值位置 private void updateClusterCenters(List<Integer> assignments, List<NodePanel.Node> nodes, List<ClusterPanel.Cluster> clusters) { int[] count = new int[clusters.size()]; int[] sumX = new int[clusters.size()]; int[] sumY = new int[clusters.size()]; for (int i = 0; i < nodes.size(); i++) { int clusterId = assignments.get(i); count[clusterId]++; sumX[clusterId] += nodes.get(i).getPos().x; sumY[clusterId] += nodes.get(i).getPos().y; } for (int i = 0; i < clusters.size(); i++) { if (count[i] > 0) { clusters.get(i).setPos(new Point(sumX[i]/count[i], sumY[i]/count[i])); } } clusterPanel.repaint(); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> new MainUI("K-Means 聚类可视化")); } }
三、关键改进说明
- 独立职责划分:
ClusterPanel和NodePanel各自管理自己的数据与绘制,代码更易维护。 - 透明面板叠加:设置
setOpaque(false)让两个面板可以叠加显示,聚类中心自然显示在节点上层,同时不影响事件传递。 - 动态更新触发:通过
repaint()方法在数据变化后立即重绘,实现K-Means迭代时的实时可视化。 - 事件逻辑优化:用
MouseAdapter简化监听器代码,单选按钮用ButtonGroup实现互斥,避免同时开启两种添加模式。
现在你可以运行重构后的代码,添加节点和聚类中心,点击"执行K-Means迭代"按钮,就能看到节点颜色随聚类动态变化,聚类中心也会移动到所属节点的均值位置,两个面板都能正常响应鼠标操作啦!
内容的提问来源于stack exchange,提问作者Станислав Иванников
相关产品推荐
相关产品推荐

