我尝试将我的 Sankey 图生成器 GUI 从 Tkinter 更改为 PyQt5。我尝试查看文档和一些实验,但找不到为什么会发生某些事情。
代码被分成框架。左侧框架获取节点标签以及流量值,右侧框架将获取特定节点信息(例如它位于哪个级别)。
我不希望它变得太复杂,所以它不需要任何新的节点行,当前只需要 2 行输入。
在左侧框架中输入节点时,该节点将显示在右侧框架中的节点特定选项。但是,当更新左侧框架上的节点文本框(通过删除值或添加多字符值)时,边距会突然增加。我找不到为什么会发生这种情况。
我也无法将标题对齐并放在最顶部。左右框架标题不想对齐,并且窗口中在其他所有内容之上有很多可用空间。这对于 PyQT5 来说正常吗?为什么我不能把标题放在最上面。
这是当前代码:
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QLineEdit, QPushButton, QSplitter, QSizePolicy
from PyQt5.QtCore import Qt
import matplotlib.pyplot as plt
from matplotlib.sankey import Sankey
class SankeyWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("SankeyFlow Diagram Creator")
self.setGeometry(100, 100, 1200, 800)
main_layout = QVBoxLayout()
splitter = QSplitter(Qt.Horizontal)
#Left Frame: Node and Flow Inputs
left_frame = QWidget()
left_layout = QVBoxLayout()
left_layout.setSpacing(2)
left_layout.setContentsMargins(0, 0, 0, 0)
header_layout = QHBoxLayout()
header_layout.setSpacing(5)
header_layout.addWidget(QLabel("Node 1"))
header_layout.addWidget(QLabel("Node 2"))
header_layout.addWidget(QLabel("Flow"))
left_layout.addLayout(header_layout)
self.node_inputs = []
self.flow_inputs = []
for i in range(2): # Initial two rows for inputs
row_layout = QHBoxLayout()
row_layout.setSpacing(5)
node1_input = QLineEdit()
node2_input = QLineEdit()
flow_input = QLineEdit()
row_layout.addWidget(node1_input)
row_layout.addWidget(node2_input)
row_layout.addWidget(flow_input)
left_layout.addLayout(row_layout)
self.node_inputs.append((node1_input, node2_input))
self.flow_inputs.append(flow_input)
generate_button = QPushButton("Generate Sankey Diagram")
generate_button.clicked.connect(self.generate_sankey)
left_layout.addWidget(generate_button)
# Connect text change events for each node input
for node1, node2 in self.node_inputs:
node1.textChanged.connect(self.update_right_frame)
node2.textChanged.connect(self.update_right_frame)
left_frame.setLayout(left_layout)
right_frame = QWidget()
right_layout = QVBoxLayout()
right_layout.setSpacing(5)
right_layout.setContentsMargins(0, 0, 0, 0)
node_values_header = QHBoxLayout()
node_values_header.setSpacing(5)
node_values_header.addWidget(QLabel("Nodes"))
node_values_header.addWidget(QLabel("Values"))
right_layout.addLayout(node_values_header)
self.node_value_inputs_layout = QVBoxLayout()
self.node_value_inputs_layout.setSpacing(5)
right_layout.addLayout(self.node_value_inputs_layout)
right_frame.setLayout(right_layout)
self.node_values = {}
splitter.addWidget(left_frame)
splitter.addWidget(right_frame)
splitter.setSizes([600, 600])
splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 1)
main_layout.addWidget(splitter)
main_layout.setSpacing(0)
main_layout.setContentsMargins(0, 0, 0, 0)
container = QWidget()
container.setLayout(main_layout)
self.setCentralWidget(container)
def update_right_frame(self):
"""Update the right frame with nodes as they are typed in the left frame."""
current_nodes = set()
for node1, node2 in self.node_inputs:
node1_label = node1.text().strip()
node2_label = node2.text().strip()
if node1_label:
current_nodes.add(node1_label)
if node2_label:
current_nodes.add(node2_label)
# Sort nodes alphabetically
sorted_nodes = sorted(current_nodes)
for node_label in sorted_nodes:
if node_label not in self.node_values:
node_row_layout = QHBoxLayout()
node_row_layout.setSpacing(5)
label_widget = QLabel(f"{node_label}:")
input_widget = QLineEdit()
input_widget.setFixedWidth(150)
input_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
node_row_layout.addWidget(label_widget)
node_row_layout.addWidget(input_widget)
self.node_value_inputs_layout.addLayout(node_row_layout)
self.node_values[node_label] = (label_widget, input_widget)
for node_label in list(self.node_values.keys()):
if node_label not in current_nodes:
label_widget, input_widget = self.node_values.pop(node_label)
label_widget.deleteLater()
input_widget.deleteLater()
self.node_value_inputs_layout.setSpacing(5)
self.node_value_inputs_layout.update()
def generate_sankey(self):
flows = []
nodes = []
for i, (node1, node2) in enumerate(self.node_inputs):
node1_label = node1.text().strip()
node2_label = node2.text().strip()
flow_value = self.flow_inputs[i].text().strip()
try:
flow_value = float(flow_value) if flow_value else 0
except ValueError:
flow_value = 0
# Create the nodes and flow entries
if node1_label and node2_label:
nodes.append([(f'{node1_label}:', abs(flow_value))])
nodes.append([(f'{node2_label}:', abs(flow_value))])
flows.append((f'{node1_label}:', f'{node2_label}:', flow_value))
# Display the Sankey diagram
plt.figure(figsize=(12, 6), dpi=144)
s = Sankey(flows=flows, nodes=nodes, node_opts=dict(label_format='{label} {value:.2f}%'))
s.draw()
plt.show()
if __name__ == "__main__":
app = QApplication([])
window = SankeyWindow()
window.show()
app.exec_()
我尝试设置间距,限制框架的窗口。删除了标题的边距,以便它们位于顶部。但这些方法都没有解决问题。
我无法使标题对齐并位于最顶部。
独立的水平布局不会正确对齐,只会使事情变得复杂。直接使用
QGridLayout
。我对代码进行了一些重构。希望它能达到你想要的效果。
在下面的代码中,有一个
headers
选项,您可以更改该选项来选择标题的位置 - 顶部还是内容附近。
from PyQt5.QtWidgets import QApplication, QGridLayout, QVBoxLayout, QWidget, QLabel, QLineEdit, QPushButton, QSplitter
from PyQt5.QtCore import Qt
import matplotlib.pyplot as plt
from matplotlib.sankey import Sankey
from enum import Enum
class Headers(Enum):
TOP = "Top"
WITH_CONTENTS = "WithContents"
class SankeyWindow(QWidget):
node_inputs: list[tuple[QLineEdit, QLineEdit]] = []
flow_inputs: list[QLineEdit] = []
node_values: dict = {}
right_layout: QGridLayout
def make_left_frame(self, headers: Headers):
frame = QWidget()
layout = QGridLayout(frame)
if headers == Headers.TOP:
header_row, stretch_row = (0, 1)
elif headers == Headers.WITH_CONTENTS:
header_row, stretch_row = (1, 0)
layout.setRowStretch(stretch_row, 10)
layout.addWidget(QLabel("Node 1"), header_row, 0)
layout.addWidget(QLabel("Node 2"), header_row, 1)
layout.addWidget(QLabel("Flow"), header_row, 2)
for i in range(2): # Initial two rows for inputs
node1_input = QLineEdit()
node2_input = QLineEdit()
flow_input = QLineEdit()
layout.addWidget(node1_input, 2+i, 0)
layout.addWidget(node2_input, 2+i, 1)
layout.addWidget(flow_input, 2+i, 2)
self.node_inputs.append((node1_input, node2_input))
self.flow_inputs.append(flow_input)
generate_button = QPushButton("Generate Sankey Diagram")
generate_button.clicked.connect(self.generate_sankey)
layout.addWidget(generate_button, layout.rowCount(), 0, 1, 3)
return frame
def make_right_frame(self, headers: Headers):
frame = QWidget()
layout = QGridLayout(frame)
self.right_layout = layout
if headers == Headers.TOP:
header_row, stretch_row = (0, 1)
elif headers == Headers.WITH_CONTENTS:
header_row, stretch_row = (1, 0)
layout.setRowStretch(stretch_row, 10)
layout.addWidget(QLabel("Nodes"), header_row, 0)
layout.addWidget(QLabel("Values"), header_row, 1)
return frame
def __init__(self):
super().__init__()
self.setWindowTitle("SankeyFlow Diagram Creator")
self.setGeometry(100, 100, 1200, 800)
left_frame = self.make_left_frame(Headers.TOP)
right_frame = self.make_right_frame(Headers.WITH_CONTENTS)
splitter = QSplitter(Qt.Horizontal)
splitter.addWidget(left_frame)
splitter.addWidget(right_frame)
splitter.setSizes([600, 600])
QVBoxLayout(self).addWidget(splitter)
# Connect text change events for each node input
for node1, node2 in self.node_inputs:
node1.textChanged.connect(self.update_right_frame)
node2.textChanged.connect(self.update_right_frame)
def update_right_frame(self):
"""Update the right frame with nodes as they are typed in the left frame."""
current_nodes = set()
for node1, node2 in self.node_inputs:
node1_label = node1.text().strip()
node2_label = node2.text().strip()
if node1_label:
current_nodes.add(node1_label)
if node2_label:
current_nodes.add(node2_label)
# Sort nodes alphabetically
sorted_nodes = sorted(current_nodes)
for node_label in sorted_nodes:
if node_label not in self.node_values:
r = self.right_layout.rowCount()
label_widget = QLabel(f"{node_label}:")
input_widget = QLineEdit()
self.right_layout.addWidget(label_widget, r, 0)
self.right_layout.addWidget(input_widget, r, 1)
self.node_values[node_label] = (label_widget, input_widget)
for node_label in list(self.node_values.keys()):
if node_label not in current_nodes:
label_widget, input_widget = self.node_values.pop(node_label)
label_widget.deleteLater()
input_widget.deleteLater()
def generate_sankey(self):
flows: list[tuple[str, str, float]] = []
nodes: list[list[tuple[str, float]]] = []
for i, (node1, node2) in enumerate(self.node_inputs):
node1_label = node1.text().strip()
node2_label = node2.text().strip()
flow_value_str = self.flow_inputs[i].text()
try:
flow_value = float(flow_value_str)
except ValueError:
flow_value = 0.0
# Create the nodes and flow entries
if node1_label and node2_label:
nodes.append([(f'{node1_label}:', abs(flow_value))])
nodes.append([(f'{node2_label}:', abs(flow_value))])
flows.append((f'{node1_label}:', f'{node2_label}:', flow_value))
# Display the Sankey diagram
plt.figure(figsize=(12, 6), dpi=144)
s = Sankey(flows=flows, nodes=nodes, node_opts={'label_format': '{label} {value:.2f}%'})
s.draw()
plt.show()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
window = SankeyWindow()
window.show()
app.exec_()
注:
要在布局中获得“空白”区域,请向布局添加拉伸。在网格布局中,这意味着在行或列上设置拉伸,并且不要向该行/列添加任何小部件:
layout = QGridLayout(myWidget)
layout.addWidget(QLabel("Header"), 0, 0)
layout.setRowStretch(1, 10)
layout.addWidget(QLabel("Footer"), 2, 0)
应用程序的主窗口应该是
QWidget
或 QDialog
,除非有特定原因需要 QMainWindow
的功能 - 通常这些是可停靠的子窗口。
构建布局时可以直接在小部件上设置布局。您无需等到布局“完成”后才设置布局。
# concise
widget = QWidget()
layout = QHBoxLayout(widget)
# verbose
widget = QWidget()
layout = QHBoxLayout()
widget.setLayout(layout)
有类型注释很有帮助。
成员变量可以在类作用域中声明和初始化。当存在类型注释时,它们“不是”类变量。他们只是成员,只是他们的声明没有隐藏在 __init__
方法中:
# more visible
class Foo:
class_field: typing.ClassVar[int] = 42 # class field
field: int = 0 # instance field
# less visible
class Foo:
class_field = 42
def __init__(self):
self.field = 0