PyQt5 的问题

问题描述 投票:0回答:1

我尝试将我的 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_()

我尝试设置间距,限制框架的窗口。删除了标题的边距,以便它们位于顶部。但这些方法都没有解决问题。

python python-3.x pyqt5
1个回答
0
投票

我无法使标题对齐并位于最顶部。

独立的水平布局不会正确对齐,只会使事情变得复杂。直接使用

QGridLayout
。我对代码进行了一些重构。希望它能达到你想要的效果。

在下面的代码中,有一个

headers
选项,您可以更改该选项来选择标题的位置 - 顶部还是内容附近。

enter image description here

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_()


注:

  1. 要在布局中获得“空白”区域,请向布局添加拉伸。在网格布局中,这意味着在行或列上设置拉伸,并且不要向该行/列添加任何小部件:

    layout = QGridLayout(myWidget)
    layout.addWidget(QLabel("Header"), 0, 0)
    layout.setRowStretch(1, 10)
    layout.addWidget(QLabel("Footer"), 2, 0)
    
  2. 应用程序的主窗口应该是

    QWidget
    QDialog
    ,除非有特定原因需要
    QMainWindow
    的功能 - 通常这些是可停靠的子窗口。

  3. 构建布局时可以直接在小部件上设置布局。您无需等到布局“完成”后才设置布局。

    # concise
    widget = QWidget()
    layout = QHBoxLayout(widget)
    
    # verbose
    widget = QWidget()
    layout = QHBoxLayout()
    widget.setLayout(layout)
    
  4. 有类型注释很有帮助。

  5. 成员变量可以在类作用域中声明和初始化。当存在类型注释时,它们“不是”类变量。他们只是成员,只是他们的声明没有隐藏在 __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
    

    
        
© www.soinside.com 2019 - 2024. All rights reserved.