Search code examples
python-3.xpyside2

How to add frames to scroll widget in PySide2


I work on note application on PySide2(For gaining experience) and I could successfully write all functions except the one that adds notes' preview in scroll widget. I designed GUI with Qt Designer and couldn't find any working solutions. My code is:

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

Function getting called when user enters proper username and password.

How can I add frame for each notes with title,context and remove button to scroll widget?

Here how it designed in Qt Designer:

And how I wanna it look like:

Marked area is 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>

Solution

  • The main problem is that you're not using layout managers.

    Exactly as their name explain, they are responsible of managing the layout, meaning that they will position and resize their children (including widgets and other "nested" layouts) according to the available size and considering the restraints of those items: some widgets can have minimum, maximum or fixed dimensions, other can try to expand to the available space left, etc.

    While there are situations for which using "fixed geometries" (meaning that the size and position of each item is not managed), it's generally good practice to use layouts instead. There are lots of reasons for which this is necessary, but the rule of thumb is that what you see on your screen is almost always never what any other user will see in theirs (different screen sizes and resolutions, different default font and font size, etc). This is almost the same reason for which every good modern website uses a "responsive design" that adapts the contents of the web pages to the screen of the user.

    In your case, two things are mandatory:

    • properly set layouts for all "container" widgets in Designer;
    • create the new notes using a layout;

    The first part is a bit boring at first, especially if you're trying to adapt a previously created UI, but it's a very important one.
    I'm attaching a revised .ui file based on your code that already has all required layouts set and includes all alignments and size policies properly set according to your needs, but I strongly suggest you to load it in Designer and compare it with what you provided in order to understand the differences. Also consider reading more about using layouts in designer.
    Basically I set:

    • a generic vertical layout for the central widget (what you named MainWidget): when there's going to be only one widget, a boxed layout is generally used; while it doesn't really matter if it's vertical or horizontal, a vertical one is statistically more commonly used;
    • a grid layout for the first page in the stacked widget; this allows adding the button on the left, the "title" label on the [remaining] center, and the scroll area occupying both "columns" of the layout (the whole horizontal space);
    • a Maximum size policy for the "add note" button, which ensures that the widget only takes the minimum required size (the size hint is the maximum size);
    • a vertical layout for the scroll area contents;
    • another vertical layout that will be used as container of the "notes";
    • a vertical spacer on bottom of the main layout of the scroll area, so that the "note container" will always use as much space as it needs and no more;
    • a grid layout for the example note widget, with the title button aligned on the left, the "remove" button on the right, and the text label occupying both columns of the grid (similarly to the above case with the scroll area);

    Note that the nested vertical layouts are only a "convenience": in reality, a single vertical layout could be used, with a spacer (a "stretch") on its bottom, and every new widget could be added by using insertWidget() with an index equal to the item count minus 1 (eg: if you have 2 widgets and you want to add another, you must use layout.insertWidget(layout.count() - 2, newWidget), since the count includes the spacer on the bottom).

    Also consider that I renamed all your widgets for consistency with the standard naming convention of general programming languages (for which variables, attributes, functions and methods should always be named starting with a lower case letter); read more about it on the Style Guide for Python Code, since Python completely (and luckily) follows those conventions.

    Resulting UI code:

    <?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>
    

    Now, the "note" part.
    Similarly to what was done with the "example" note above, you must create the new note using a grid layout.

    For [hard] learning purposes, I'm not going to explain every part of the code, as it's very important that you carefully study every function used in order to completely understand it.
    Please, really, don't just copy and paste it, try to understand what happens there.

    Also note that I did not use the ui object, but I inherited from the form class instead. This is quite useful, as you can directly access all widgets from self (instead of self.ui).
    As already explained above, I'm using object names that begin with lower case letters.

    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)
    

    Since it might be useful to access the individual widgets of each frame, a common approach is to create a class. This is a very important aspect, as it is more compliant with the OOP pattern and also allows accessing each element of the "note" widget.

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

    Finally, as you've noticed, I changed a lot of your original code. This wasn't just a "liberty", but a responsibility, for you, for users of your programs, and for anybody that could read your code (including you!).

    1. naming patterns are not just an annoying convention, they are a fundamental assistance: being able to recognize at a glance if an object is a class or an instance is very important, as it greatly improves code understanding and, therefore, debugging; when you read code you must be able to focus on what the code does, not what the code refers to;
    2. "generic" (or "universal") stylesheet declaration often cause issues in Qt; using appropriate selectors ensures that only the proper objects get their properties set;
    3. setting the Qt objectName is generally unnecessary for PyQt code (that's why I removed all of them); uic does it for compliancy with Qt, but since uic also automatically creates instance attributes for widgets, they are usually not useful from the python perspective;
    4. there are very rare and very specific cases for which using fixed geometries (explicitly setting position and size) of widgets is required; the general rule is that you could really use fixed geometries only if you really know what you're doing and why, considering a lot of aspects: screen geometry, system defaults, OS sizing patterns, user customization, screen DPI, default font (including spacings at different point sizes), etc, etc;