如何在 PySide2 中添加框架以滚动小部件

How to add frames to scroll widget in PySide2

我在 PySide2 上开发笔记应用程序(为了获得经验),我可以成功编写所有功能,除了在滚动小部件中添加笔记预览的功能。我用 Qt Designer 设计了 ​​GUI,但找不到任何可行的解决方案。我的代码是:

from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QCoreApplication, QRect, Qt
from PySide2.QtWidgets import QApplication, QFrame, QLabel, QMessageBox, QPushButton
import MyNotesUi


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        # Setup ui
        QMainWindow = QtWidgets.QMainWindow
        QMainWindow.__init__(self)
        self.ui = MyNotesUi.Ui_MyNotes()
        self.ui.setupUi(self)


#The function that should add notes.
    def loadNotes(self):
        cursor.execute("SELECT * FROM notes WHERE username=?",(self.ui.LoginUsernameLineEdit.text(),))
        notes=cursor.fetchall()
        x=111 #----|
        y=21  #----|---Those are positions.
        z=61  #----|
        for note in notes:
            frame = QFrame(self.ui.NotesScrollWidget)
            frame.setObjectName(u"NoteFrame")
            frame.setGeometry(QRect(0, 0, 731, x))
            frame.setFrameShape(QFrame.StyledPanel)
            frame.setFrameShadow(QFrame.Raised)
            title = QPushButton(frame)
            title.setObjectName(u"NoteTitleButton")
            title.setGeometry(QRect(10, 10, 51, y))
            font = QtGui.QFont()
            font.setFamily(u"SF Pro Display")
            font.setPointSize(13)
            font.setBold(False)
            font.setItalic(False)
            font.setWeight(50)
            title.setFont(font)
            title.setCursor(QtGui.QCursor(Qt.PointingHandCursor))
            title.setStyleSheet(u"border:none;\n"
    "font: 13pt \"SF Pro Display\";")
            context = QLabel(frame)
            context.setObjectName(u"NoteText")
            context.setGeometry(QRect(10, 40, 691, z))
            context.setStyleSheet(u"font: 10pt \"SF Pro Display\";")
            context.setAlignment(QtGui.Qt.AlignLeading|QtGui.Qt.AlignLeft|QtGui.Qt.AlignTop)
            remove_button = QPushButton(frame)
            remove_button.setObjectName(u"NoteRemoveButton")
            remove_button.setGeometry(QRect(650, 0, 80, 25))
            remove_button.setCursor(QtGui.QCursor(QtGui.Qt.PointingHandCursor))
            remove_button.setStyleSheet(u"font: 10pt \"SF Pro Display\";")
            title.setText(QCoreApplication.translate("MyNotes", note[1], None))
            context.setText(QCoreApplication.translate("MyNotes", note[2], None))
            remove_button.setText(QCoreApplication.translate("MyNotes", u"Remove", None))
            self.ui.NotesScrollArea.addScrollBarWidget(self.ui.NotesScrollWidget,Qt.AlignLeft|Qt.AlignTop)
            self.ui.NotesScrollArea.setWidget(self.ui.NotesScrollWidget)
            y+=10
            z+=10
            x+=10

当用户输入正确的用户名和密码时调用函数。

如何为带有标题、上下文和删除按钮的每个笔记添加框架以滚动小部件?

这是它在 Qt Designer 中的设计方式:

以及我想要的样子:

标记区域为QScrollArea

UI:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MyNotes</class>
 <widget class="QMainWindow" name="MyNotes">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>730</width>
    <height>538</height>
   </rect>
  </property>
  <property name="minimumSize">
   <size>
    <width>730</width>
    <height>538</height>
   </size>
  </property>
  <property name="maximumSize">
   <size>
    <width>730</width>
    <height>538</height>
   </size>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="MainWidget">
   <property name="minimumSize">
    <size>
     <width>730</width>
     <height>538</height>
    </size>
   </property>
   <property name="maximumSize">
    <size>
     <width>730</width>
     <height>538</height>
    </size>
   </property>
   <widget class="QStackedWidget" name="Pages">
    <property name="geometry">
     <rect>
      <x>0</x>
      <y>0</y>
      <width>731</width>
      <height>541</height>
     </rect>
    </property>
    <property name="currentIndex">
     <number>0</number>
    </property>
    <widget class="QWidget" name="NotesPage">
     <widget class="QLabel" name="MyNotesTitle">
      <property name="geometry">
       <rect>
        <x>310</x>
        <y>10</y>
        <width>92</width>
        <height>25</height>
       </rect>
      </property>
      <property name="font">
       <font>
        <family>Open Sans</family>
        <pointsize>16</pointsize>
        <weight>3</weight>
        <italic>false</italic>
        <bold>false</bold>
       </font>
      </property>
      <property name="styleSheet">
       <string notr="true">font: 25 16pt &quot;Open Sans&quot;;</string>
      </property>
      <property name="text">
       <string>MyNotes</string>
      </property>
      <property name="alignment">
       <set>Qt::AlignCenter</set>
      </property>
     </widget>
     <widget class="QScrollArea" name="NotesScrollArea">
      <property name="geometry">
       <rect>
        <x>-1</x>
        <y>49</y>
        <width>731</width>
        <height>491</height>
       </rect>
      </property>
      <property name="widgetResizable">
       <bool>true</bool>
      </property>
      <widget class="QWidget" name="NotesScrollWidget">
       <property name="geometry">
        <rect>
         <x>0</x>
         <y>0</y>
         <width>729</width>
         <height>489</height>
        </rect>
       </property>
       <widget class="QFrame" name="ExampleNoteFrame">
        <property name="geometry">
         <rect>
          <x>0</x>
          <y>0</y>
          <width>731</width>
          <height>111</height>
         </rect>
        </property>
        <property name="frameShape">
         <enum>QFrame::StyledPanel</enum>
        </property>
        <property name="frameShadow">
         <enum>QFrame::Raised</enum>
        </property>
        <widget class="QPushButton" name="NoteTitleButton">
         <property name="geometry">
          <rect>
           <x>10</x>
           <y>10</y>
           <width>51</width>
           <height>21</height>
          </rect>
         </property>
         <property name="font">
          <font>
           <family>SF Pro Display</family>
           <pointsize>13</pointsize>
           <weight>50</weight>
           <italic>false</italic>
           <bold>false</bold>
          </font>
         </property>
         <property name="cursor">
          <cursorShape>PointingHandCursor</cursorShape>
         </property>
         <property name="styleSheet">
          <string notr="true">border:none;
font: 13pt &quot;SF Pro Display&quot;;</string>
         </property>
         <property name="text">
          <string>Title</string>
         </property>
        </widget>
        <widget class="QLabel" name="NoteText">
         <property name="geometry">
          <rect>
           <x>10</x>
           <y>40</y>
           <width>691</width>
           <height>61</height>
          </rect>
         </property>
         <property name="styleSheet">
          <string notr="true">font: 10pt &quot;SF Pro Display&quot;;</string>
         </property>
         <property name="text">
          <string>TextLabel</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
         </property>
        </widget>
        <widget class="QPushButton" name="NoteRemoveButton">
         <property name="geometry">
          <rect>
           <x>650</x>
           <y>0</y>
           <width>80</width>
           <height>25</height>
          </rect>
         </property>
         <property name="cursor">
          <cursorShape>PointingHandCursor</cursorShape>
         </property>
         <property name="text">
          <string>Remove</string>
         </property>
        </widget>
       </widget>
      </widget>
     </widget>
     <widget class="QPushButton" name="AddNewNoteButton">
      <property name="geometry">
       <rect>
        <x>0</x>
        <y>10</y>
        <width>92</width>
        <height>25</height>
       </rect>
      </property>
      <property name="cursor">
       <cursorShape>PointingHandCursor</cursorShape>
      </property>
      <property name="styleSheet">
       <string notr="true">font: 10pt &quot;SF Pro Display&quot;;</string>
      </property>
      <property name="text">
       <string>Add new note</string>
      </property>
     </widget>
    </widget>
   </widget>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

主要问题是您没有使用 layout managers

正如他们的名字所解释的那样,他们负责管理 布局,这意味着他们将定位和调整他们的children(包括小部件 其他“嵌套”布局)根据可用大小并考虑这些项目的限制:一些小部件可以具有最小、最大或固定尺寸,其他可以尝试扩展到可用 space 左侧等

虽然在某些情况下需要使用“固定几何图形”(这意味着每个项目的大小和位置不受 管理),但通常最好改用布局。有 很多 的理由表明这是必要的,但经验法则是您在屏幕上看到的几乎总是 从不 任何其他用户将在他们的中看到(不同的屏幕尺寸 分辨率,不同的默认字体和字体大小等)。这几乎与每个优秀的现代网站都使用“响应式设计”使网页内容适应用户屏幕的原因几乎相同。

在你的情况下,有两件事是强制性的:

  • 在设计器中为所有“容器”小部件正确设置布局;
  • 使用布局创建新的笔记

第一部分一开始有点无聊,尤其是如果您尝试改编之前创建的 UI,但它是 非常重要 的部分。
我根据您的代码附加了一个修改后的 .ui 文件,该文件已经设置了所有必需的布局,并包括根据您的需要正确设置的所有对齐方式和大小策略,但我强烈建议您在 Designer 中加载它并进行比较与您提供的内容进行比较,以了解差异。还可以考虑阅读更多关于 using layouts in designer.
的内容 基本上我设置:

  • 中央小部件的通用垂直布局(您命名的MainWidget):当只有一个小部件时,通常使用盒装布局;虽然它是垂直的还是水平的并不重要,但从统计上讲,垂直的更常用;
  • a grid 堆叠小部件中第一页的布局;这允许在左侧添加按钮,在 [剩余] 中心添加“标题”标签,以及占据布局的两个“列”的滚动区域(整个水平 space);
  • “添加注释”按钮的 Maximum 大小策略,确保小部件仅采用所需的最小大小(大小提示最大 尺寸);
  • 滚动区域内容的垂直布局;
  • 另一种 将用作“注释”容器的垂直布局;
  • 滚动区域主布局底部的垂直 spacer,这样“笔记容器”将始终根据需要使用尽可能多的 space,不再使用;
  • 示例笔记小部件的网格布局,标题按钮对齐在左侧,“删除”按钮在右侧,文本标签占据了两列网格(类似于上面滚动区域的情况);

请注意,嵌套的垂直布局只是一种“方便”:实际上,可以使用单个垂直布局,在其底部带有 spacer(“拉伸”),并且每个新的可以使用 insertWidget() 添加小部件,索引等于项目计数减 1(例如:如果您有 2 个小部件并且想添加另一个,则必须使用 layout.insertWidget(layout.count() - 2, newWidget),因为计数包括spacer 在底部)。

同时考虑到我重命名了你的所有小部件以与通用编程语言的标准命名约定保持一致(变量、属性、函数和方法应该 总是 以开头命名小写字母);在 Style Guide for Python Code 上阅读更多相关信息,因为 Python 完全(幸运地)遵循这些约定。

结果 UI 代码:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>myNotes</class>
 <widget class="QMainWindow" name="myNotes">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>739</width>
    <height>538</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="mainWidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QStackedWidget" name="pages">
      <property name="currentIndex">
       <number>0</number>
      </property>
      <widget class="QWidget" name="notesPage">
       <layout class="QGridLayout" name="gridLayout">
        <item row="0" column="0">
         <widget class="QPushButton" name="addNewNoteButton">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
          </property>
          <property name="cursor">
           <cursorShape>PointingHandCursor</cursorShape>
          </property>
          <property name="styleSheet">
           <string notr="true">font: 10pt &quot;SF Pro Display&quot;;</string>
          </property>
          <property name="text">
           <string>Add new note</string>
          </property>
         </widget>
        </item>
        <item row="0" column="1">
         <widget class="QLabel" name="myNotesTitle">
          <property name="font">
           <font>
            <family>Open Sans</family>
            <pointsize>16</pointsize>
            <weight>3</weight>
            <italic>false</italic>
            <bold>false</bold>
           </font>
          </property>
          <property name="styleSheet">
           <string notr="true">font: 25 16pt &quot;Open Sans&quot;;</string>
          </property>
          <property name="text">
           <string>MyNotes</string>
          </property>
          <property name="alignment">
           <set>Qt::AlignCenter</set>
          </property>
         </widget>
        </item>
        <item row="1" column="0" colspan="2">
         <widget class="QScrollArea" name="notesScrollArea">
          <property name="widgetResizable">
           <bool>true</bool>
          </property>
          <widget class="QWidget" name="notesScrollWidget">
           <property name="geometry">
            <rect>
             <x>0</x>
             <y>0</y>
             <width>711</width>
             <height>475</height>
            </rect>
           </property>
           <layout class="QVBoxLayout" name="verticalLayout_2">
            <item>
             <layout class="QVBoxLayout" name="scrollContainerLayout">
              <item>
               <widget class="QFrame" name="exampleNoteFrame">
                <property name="frameShape">
                 <enum>QFrame::StyledPanel</enum>
                </property>
                <property name="frameShadow">
                 <enum>QFrame::Raised</enum>
                </property>
                <layout class="QGridLayout" name="gridLayout_2">
                 <item row="0" column="0" alignment="Qt::AlignLeft">
                  <widget class="QPushButton" name="noteTitleButton">
                   <property name="font">
                    <font>
                     <family>SF Pro Display</family>
                     <pointsize>13</pointsize>
                     <weight>50</weight>
                     <italic>false</italic>
                     <bold>false</bold>
                    </font>
                   </property>
                   <property name="cursor">
                    <cursorShape>PointingHandCursor</cursorShape>
                   </property>
                   <property name="styleSheet">
                    <string notr="true">border:none;
font: 13pt &quot;SF Pro Display&quot;;</string>
                   </property>
                   <property name="text">
                    <string>Title</string>
                   </property>
                  </widget>
                 </item>
                 <item row="0" column="1" alignment="Qt::AlignRight">
                  <widget class="QPushButton" name="noteRemoveButton">
                   <property name="cursor">
                    <cursorShape>PointingHandCursor</cursorShape>
                   </property>
                   <property name="text">
                    <string>Remove</string>
                   </property>
                  </widget>
                 </item>
                 <item row="1" column="0" colspan="2">
                  <widget class="QLabel" name="noteText">
                   <property name="styleSheet">
                    <string notr="true">font: 10pt &quot;SF Pro Display&quot;;</string>
                   </property>
                   <property name="text">
                    <string>TextLabel</string>
                   </property>
                   <property name="alignment">
                    <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
                   </property>
                  </widget>
                 </item>
                </layout>
               </widget>
              </item>
             </layout>
            </item>
            <item>
             <spacer name="verticalSpacer">
              <property name="orientation">
               <enum>Qt::Vertical</enum>
              </property>
              <property name="sizeHint" stdset="0">
               <size>
                <width>20</width>
                <height>40</height>
               </size>
              </property>
             </spacer>
            </item>
           </layout>
          </widget>
         </widget>
        </item>
       </layout>
      </widget>
     </widget>
    </item>
   </layout>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

现在,“注释”部分。
与上面的“示例”笔记所做的类似,您必须使用网格布局创建新笔记。

出于 [困难] 学习目的,我不打算解释代码的每一部分,因为 非常 重要的是您仔细研究使用的每个函数以便完全理解明白了。
真的,请不要只是复制和粘贴它,试着了解那里发生了什么。

另请注意,我没有使用uiobject,而是继承自class形式。这非常有用,因为您可以直接从 self(而不是 self.ui)访问所有小部件。
如上所述,我使用的是 object 以小写字母开头的名称。

class MainWindow(QtWidgets.QMainWindow, MyNotesUi.Ui_MyNotes):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.addNewNoteButton.clicked.connect(self.loadNotes)

    def loadNotes(self):
        cursor.execute(
            "SELECT * FROM notes WHERE username=?", 
            (self.loginUsernameLineEdit.text(),)
        )
        for note in cursor.fetchall():
            # the container frame
            frame = QtWidgets.QFrame()
            frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
            frame.setFrameShadow(QtWidgets.QFrame.Raised)
            # creating a layout with a widget arguments automatically sets  
            # the layout for that widget
            frameLayout = QtWidgets.QGridLayout(frame)

            title = QtWidgets.QPushButton(note[1])
            title.setFont(QtGui.QFont("SF Pro Display", 13))
            title.setCursor(QtCore.Qt.PointingHandCursor)
            title.setStyleSheet('''
                QPushButton {
                    border: none;
                    font: 13pt "SF Pro Display";
                }
            ''')
            frameLayout.addWidget(title, 0, 0, alignment=QtCore.Qt.AlignLeft)

            remove_button = QtWidgets.QPushButton("Remove")
            remove_button.setCursor(QtCore.Qt.PointingHandCursor)
            remove_button.setStyleSheet('''
                QPushButton {
                    font: 10pt "SF Pro Display";
                }
            ''')
            frameLayout.addWidget(
                remove_button, 0, 1, alignment=QtCore.Qt.AlignRight)

            context = QtWidgets.QLabel(note[2])
            context.setStyleSheet('''
                QLabel {
                    font: 10pt "SF Pro Display";
                }
            ''')
            context.setAlignment(
                QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
            # add the label by making it fill the second row and *both* columns
            frameLayout.addWidget(context, 1, 0, 1, 2)

            # add the frame to the nested layout
            self.scrollContainerLayout.addWidget(frame)

因为它 m访问每个框架的各个小部件可能很有用,一种常见的方法是创建一个 class。这是一个非常重要的方面,因为它更符合 OOP 模式,并且还允许访问“笔记”小部件的每个元素。

class Note(QtWidgets.QFrame):
    def __init__(self, note):
        super().__init__()
        self.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.setFrameShadow(QtWidgets.QFrame.Raised)
        layout = QtWidgets.QGridLayout(self)

        self.title = QtWidgets.QPushButton(note[1])
        self.title.setFont(QtGui.QFont("SF Pro Display", 13))
        self.title.setCursor(QtCore.Qt.PointingHandCursor)
        self.title.setStyleSheet('''
            QPushButton {
                border: none;
                font: 13pt "SF Pro Display";
            }
        ''')
        layout.addWidget(self.title, 0, 0, alignment=QtCore.Qt.AlignLeft)

        self.remove_button = QtWidgets.QPushButton("Remove")
        self.remove_button.setCursor(QtCore.Qt.PointingHandCursor)
        self.remove_button.setStyleSheet('''
            QPushButton {
                font: 10pt "SF Pro Display";
            }
        ''')
        layout.addWidget(self.remove_button, 0, 1, alignment=QtCore.Qt.AlignRight)

        self.context = QtWidgets.QLabel(note[2])
        self.context.setStyleSheet('''
            QLabel {
                font: 10pt "SF Pro Display";
            }
        ''')
        self.context.setAlignment(
            QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
        layout.addWidget(self.context, 1, 0, 1, 2)


class MainWindow(QtWidgets.QMainWindow, MyNotesUi.Ui_MyNotes):
    def __init__(self):
        super().__init__()
        self.noteWidgets = []
        self.setupUi(self)
        self.addNewNoteButton.clicked.connect(self.loadNotes)

    def loadNotes(self):
        cursor.execute(
            "SELECT * FROM notes WHERE username=?", 
            (self.loginUsernameLineEdit.text(),)
        )
        for note in cursor.fetchall():
            noteWidget = Note(note)
            self.scrollContainerLayout.addWidget(noteWidget)
            self.noteWidgets.append(noteWidget)

        for noteWidget in self.noteWidgets:
            print(noteWidget.title.text())

最后,正如您所注意到的,我对您的原始代码进行了很多更改。这不仅仅是一种“自由”,而是一种责任,对你、对你的程序的用户,对任何可以阅读你的代码的人(包括你!)。

  1. 命名模式不仅仅是一个烦人的约定,它们是一个基本的帮助:能够一眼识别 object 是 class 还是实例是 非常 重要,因为它极大地提高了代码理解能力,因此也提高了调试能力;当您阅读代码时,您必须能够专注于代码的作用,而不是代码所指的内容;
  2. “通用”(或“通用”)样式表声明通常会在 Qt 中引起问题;使用适当的 selectors 可确保只有适当的 object 才能设置其属性;
  3. 设置 Qt objectName 对于 PyQt 代码通常是不必要的(这就是我删除所有这些代码的原因); uic 这样做是为了与 Qt 兼容,但由于 uic 还会自动为小部件创建实例属性,因此从 python 的角度来看它们通常没有用;
  4. 在非常罕见和非常特殊的情况下,需要使用小部件的固定几何形状(明确设置位置和大小);一般规则是,只有当您 真正 知道自己在做什么以及为什么要这样做时,您才能真正使用固定几何形状,考虑 lot 个方面:屏幕几何形状、系统默认值、OS 大小调整模式、用户自定义、屏幕 DPI、默认字体(包括不同点大小的间距)等等;