PyQt5:使用添加的 lineEdit 子类化 QSlider(用于在 QtDesigner 中推广)?

PyQt5: subclassing QSlider (for promote in QtDesigner) with added lineEdit?

基本上,我想在 QtDesigner 中将一些 QSlider 添加到 window,然后用包含滑块的 class“替换”它们,在滑块下方是一个文本框(一个行编辑)。


编辑:我为什么要这样做:当我使用 QtDesigner 时,我可以放置滑块,并具有布局的近似可视化,因为它将在最终应用程序中出现:

这就是为什么我想首先使用 QtDesigner - 获得最终布局的近似可视化,因为它将在应用程序中。由于我想用某种基于滑块的小部件替换这些滑块,因此首先让滑块出现在视图中对我更有帮助。

但是,如果我必须将 QWidget 作为替换滑块所在位置的起点,则 QtDesigner 视图如下所示:

换句话说,之前显示滑块的 space 现在是空的 - 所以现在我不再有最终 GUI 布局的预览,这有点违背了使用 QtDesigner 的目的对我来说(我还不如走艰难的路,尝试完全用代码绘制 GUI,没有任何视觉反馈,可能会消耗所有时间)。


到目前为止,我设法做到了这一点 - 我在 test2.py 中实现了 QSlider 的一个名为“VertSlider”的子class,然后将 QtDesigner 中的 QSliders 提升到这个 class在 test2.ui:

有趣的是,它有些作用 - 如果您尝试仔细观察右侧两个滑块的中心,您可以在滑块的中心看到线条编辑的轮廓。

但我不希望那样,显然 - 我希望在底部进行行编辑,它应该占据与原始滑块外观(如 QtDesigner 中指定的)一样多的垂直 space根据需要,然后实际滑块应填满垂直 space 的其余部分(如屏幕截图左侧所示)。

我想,部分问题在于 QSlider 似乎没有 .layout() - 默认情况下 returns "None";我试图强制一个,但这显然行不通。

通过 found this quote from documentation:

If there already is a layout manager installed on this widget, QWidget won't let you install another. You must first delete the existing layout manager (returned by layout()) before you can call setLayout() with the new layout.

很明显,QSlider 没有默认的布局管理器....根据 and Qt add a widget inside another widget? - it seems in that case, I'd have to either "Subclass ... and override paintEvent()" or "Use a proxy style/drawComplexControl()"; by the terminology in https://www.learnpyqt.com/courses/custom-widgets/creating-your-own-custom-widgets/ 判断,这将是一个“自定义绘制的小部件”,但我真的希望我可以“只是”做一个“复合”小部件:只是以某种方式从 QtDesigner 中基于 QSlider 的规范 -> 到 QSLider+QLineEdit 小部件,而无需处理自定义绘画。

当然,原则上我可以 subclass QWidget,然后使用 layout.addWidget 的方法会起作用——但我不能使用 subclass 来“提升”什么是在 QtDesigner 中作为 QSlider 放置。

那么,创建 QSlider subclass 的最简单方法是什么,只需在滑块底部添加一个行编辑文本框,即可用作 class将 QSlider 提升到 QtDesigner 中?

test2.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>436</width>
    <height>354</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout">
    <item row="0" column="0">
     <widget class="QSplitter" name="splitter">
      <property name="orientation">
       <enum>Qt::Horizontal</enum>
      </property>
      <widget class="QFrame" name="frame">
       <property name="frameShape">
        <enum>QFrame::StyledPanel</enum>
       </property>
       <property name="frameShadow">
        <enum>QFrame::Sunken</enum>
       </property>
       <layout class="QGridLayout" name="gridLayout_2">
        <item row="0" column="0">
         <layout class="QVBoxLayout" name="verticalLayout">
          <item>
           <widget class="QLabel" name="label">
            <property name="text">
             <string>Want this:</string>
            </property>
           </widget>
          </item>
          <item>
           <layout class="QHBoxLayout" name="horizontalLayout_2">
            <item>
             <layout class="QVBoxLayout" name="verticalLayout_3">
              <item alignment="Qt::AlignHCenter">
               <widget class="QSlider" name="verticalSlider">
                <property name="orientation">
                 <enum>Qt::Vertical</enum>
                </property>
               </widget>
              </item>
              <item alignment="Qt::AlignHCenter">
               <widget class="QLineEdit" name="lineEdit">
                <property name="maximumSize">
                 <size>
                  <width>50</width>
                  <height>16777215</height>
                 </size>
                </property>
               </widget>
              </item>
             </layout>
            </item>
            <item>
             <spacer name="horizontalSpacer">
              <property name="orientation">
               <enum>Qt::Horizontal</enum>
              </property>
              <property name="sizeHint" stdset="0">
               <size>
                <width>40</width>
                <height>20</height>
               </size>
              </property>
             </spacer>
            </item>
           </layout>
          </item>
         </layout>
        </item>
       </layout>
      </widget>
      <widget class="QFrame" name="frame_2">
       <property name="frameShape">
        <enum>QFrame::StyledPanel</enum>
       </property>
       <property name="frameShadow">
        <enum>QFrame::Sunken</enum>
       </property>
       <layout class="QGridLayout" name="gridLayout_3">
        <item row="0" column="0">
         <layout class="QVBoxLayout" name="verticalLayout_2">
          <item>
           <widget class="QLabel" name="label_2">
            <property name="text">
             <string>... by promoting these QSliders in QtDesigner:</string>
            </property>
           </widget>
          </item>
          <item>
           <layout class="QHBoxLayout" name="horizontalLayout">
            <item>
             <widget class="VertSlider" name="verticalSlider_2">
              <property name="orientation">
               <enum>Qt::Vertical</enum>
              </property>
             </widget>
            </item>
            <item>
             <widget class="VertSlider" name="verticalSlider_3">
              <property name="orientation">
               <enum>Qt::Vertical</enum>
              </property>
             </widget>
            </item>
           </layout>
          </item>
         </layout>
        </item>
       </layout>
      </widget>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>436</width>
     <height>21</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <customwidgets>
  <customwidget>
   <class>VertSlider</class>
   <extends>QSlider</extends>
   <header>test2</header>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

test2.py

import sys
from PyQt5 import QtCore, QtWidgets, QtGui, uic
from PyQt5.QtCore import pyqtSlot

class VertSlider(QtWidgets.QSlider):
  def __init__(self, *args, **kwargs):
    QtWidgets.QSlider.__init__(self, *args, **kwargs)
    print(self.layout()) # None
    # so, trying to force a layout here, so I could add a line edit - but it doesn't quite work:
    self.layout = QtWidgets.QVBoxLayout(self)
    self.label = QtWidgets.QLineEdit(self)
    self.label.setText("aa")
    self.label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)
    self.layout.addWidget(self.label)
    self.setLayout(self.layout)
    print(self.layout, self.layout.count(), self.label.width(), self.label.height(), self.label.x(), self.label.y()) # <PyQt5.QtWidgets.QVBoxLayout object at 0x0000000006681790> 1 100 30 0 0


class MyMainWindow(QtWidgets.QMainWindow):
  def __init__(self):
    super(MyMainWindow, self).__init__()
    uic.loadUi('test2.ui', self)
    self.show()

def main():
  app = QtWidgets.QApplication(sys.argv)
  window = MyMainWindow()
  sys.exit(app.exec_())

if __name__ == "__main__":
  main()

您不想推广 QSlider,而是推广包含 QSlider 的 class,因此解决方案是创建该小部件:

import sys
from PyQt5 import QtCore, QtWidgets, QtGui, uic


class CustomWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.slider = QtWidgets.QSlider(orientation=QtCore.Qt.Vertical)
        self.lineedit = QtWidgets.QLineEdit(text="aa")
        # self.lineedit.setMaximumWidth(50)

        hlay = QtWidgets.QHBoxLayout(self)

        vlay = QtWidgets.QVBoxLayout()
        vlay.addWidget(self.slider, 0, QtCore.Qt.AlignHCenter)
        vlay.addWidget(self.lineedit, 0, QtCore.Qt.AlignHCenter)

        hlay.addLayout(vlay)
        spacer_item = QtWidgets.QSpacerItem(
            40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
        )
        hlay.addItem(spacer_item)


class MyMainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MyMainWindow, self).__init__()
        uic.loadUi("test2.ui", self)
        self.show()


def main():
    app = QtWidgets.QApplication(sys.argv)
    window = MyMainWindow()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>436</width>
    <height>354</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout">
    <item row="0" column="0">
     <widget class="QSplitter" name="splitter">
      <property name="orientation">
       <enum>Qt::Horizontal</enum>
      </property>
      <widget class="QFrame" name="frame">
       <property name="frameShape">
        <enum>QFrame::StyledPanel</enum>
       </property>
       <property name="frameShadow">
        <enum>QFrame::Sunken</enum>
       </property>
       <layout class="QGridLayout" name="gridLayout_2">
        <item row="0" column="0">
         <layout class="QVBoxLayout" name="verticalLayout">
          <item>
           <widget class="QLabel" name="label">
            <property name="text">
             <string>Want this:</string>
            </property>
           </widget>
          </item>
          <item>
           <layout class="QHBoxLayout" name="horizontalLayout_2">
            <item>
             <layout class="QVBoxLayout" name="verticalLayout_3">
              <item alignment="Qt::AlignHCenter">
               <widget class="QSlider" name="verticalSlider">
                <property name="orientation">
                 <enum>Qt::Vertical</enum>
                </property>
               </widget>
              </item>
              <item alignment="Qt::AlignHCenter">
               <widget class="QLineEdit" name="lineEdit">
                <property name="maximumSize">
                 <size>
                  <width>50</width>
                  <height>16777215</height>
                 </size>
                </property>
               </widget>
              </item>
             </layout>
            </item>
            <item>
             <spacer name="horizontalSpacer">
              <property name="orientation">
               <enum>Qt::Horizontal</enum>
              </property>
              <property name="sizeHint" stdset="0">
               <size>
                <width>40</width>
                <height>20</height>
               </size>
              </property>
             </spacer>
            </item>
           </layout>
          </item>
         </layout>
        </item>
       </layout>
      </widget>
      <widget class="QFrame" name="frame_2">
       <property name="frameShape">
        <enum>QFrame::StyledPanel</enum>
       </property>
       <property name="frameShadow">
        <enum>QFrame::Sunken</enum>
       </property>
       <layout class="QGridLayout" name="gridLayout_3">
        <item row="0" column="0">
         <layout class="QVBoxLayout" name="verticalLayout_2">
          <item>
           <widget class="QLabel" name="label_2">
            <property name="text">
             <string>... by promoting these QSliders in QtDesigner:</string>
            </property>
           </widget>
          </item>
          <item>
           <layout class="QHBoxLayout" name="horizontalLayout">
            <item>
             <widget class="CustomWidget" name="widget_2" native="true"/>
            </item>
            <item>
             <widget class="CustomWidget" name="widget" native="true"/>
            </item>
           </layout>
          </item>
         </layout>
        </item>
       </layout>
      </widget>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>436</width>
     <height>26</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <customwidgets>
  <customwidget>
   <class>CustomWidget</class>
   <extends>QWidget</extends>
   <header>test2</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

我可能应该早点说,在 QtDesigner 中从滑块的图像开始对我来说很重要,因为它可以帮助我在视觉上设计 GUI 界面。

@eyllanesc 的解决方案在技术上可行 - 但是,我在 QtDesigner 中有空的小部件,这对我的视觉设计没有帮助。

但是,我从@bfris 的评论中尝试了这个建议:

You can draw the QWidget in Qt Designer with it's subwidgets and then Promote it to your custom widget

...我想我得到了一个混合了@eyllanesc 的答案和这种方法的解决方案,所以我既可以在 QtDesigner 中看到滑块,又可以将 QWidget 子类化。

首先,在添加了一个小部件之后(我只是使用@eyllanesc 答案中的 .ui 作为起点),只需在 QtDesigner 中拖动一个垂直滑块作为其 child:

然而,此时新添加的滑块不会“对齐”,因为托管它的小部件(parent 小部件)没有布局(如红色圆圈 cross-out 图标位于 Object QtDesigner 中的检查器树视图中的小部件图标的右下角)。在这里,只需 right-click Object Inspector 中的 parent 小部件,然后选择布局(这里我选择水平布局):

完成后,widget图标右下方的红色圆形cross-out图标消失,滑块在Qt Designer中看起来合理:

现在我们可以试试代码了。基本上,它与@eyllanesc 的回答中的解决方案相同,除了:

  • 在我们做新的“子类”布局之前,必须删除 QtDesigner 中添加到 QWidget 的布局和滑块
  • 但是,当小部件子类 __init__ 运行 时,它 知道它有来自 QtDesigner 的 children(布局和滑块)
  • 因此,init 必须 运行 延迟,所以我们可以先从 QtDesigner(布局和滑块)中删除 children - 然后我们可以用“子类”填充children(新布局、滑块和行编辑)
  • 然而,在 PyQt5 中通常我们只有 deleteLater - 但如果我们从 QtDesigner 中删除布局,那么我们将得到 "QLayout: Attempting to add QLayout "" to CustomWidget"widget_2”,当我们尝试添加新的子类布局时,它已经有一个布局”;所以,我们必须使用 sip 模块来删除布局“现在”

考虑到所有这些,下面是 .py 和 .ui 代码,它的结果是这样的 GUI:

...这对我来说基本上已经足够好了,因为最终的 GUI 与我在 QtDesigner 中看到的 GUI 非常相似。

test2.py

import sys
from PyQt5 import QtCore, QtWidgets, QtGui, uic
import sip

class CustomWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.printInfo("__init__:")
        QtCore.QTimer.singleShot(10, self.delayedInit) # 10 ms later

    def printInfo(self, label=None):
        if label is None:
          label = "No-label"
        print(label, self.layout(), self.children())
        # above may print:
        # __init__: None []
        # delayedInit: <PyQt5.QtWidgets.QHBoxLayout object at 0x000000000667eb80> [<PyQt5.QtWidgets.QHBoxLayout object at 0x000000000667eb80>, <PyQt5.QtWidgets.QSlider object at 0x000000000667ec10>]

        self.dumpObjectTree() # auto-prints to stdout
        self.dumpObjectInfo() # auto-prints to stdout

    def delayedInit(self):
        self.printInfo("delayedInit:")

        # delete contents of the pre-existing mock-up widget from QtDesigner
        for tchild in reversed(self.children()):
          #~ #tchild.setParent(None) # segfault
          #if type(tchild) is not QtWidgets.QHBoxLayout: # works, but better compare with self.layout():
          if tchild is not self.layout():
              tchild.deleteLater()
          else:
              print("Not deletingLater", tchild)
        # delete the layout of the pre-existing mock-up widget from QtDesigner;
        # must be "now" (via sip), not "later", else: "QLayout: Attempting to add QLayout "" to CustomWidget "widget_2", which already has a layout"
        if self.layout() is not None:
            sip.delete(self.layout())

        self.slider = QtWidgets.QSlider(orientation=QtCore.Qt.Vertical)
        self.lineedit = QtWidgets.QLineEdit(text="aa")
        # self.lineedit.setMaximumWidth(50)

        hlay = QtWidgets.QHBoxLayout(self)

        vlay = QtWidgets.QVBoxLayout()
        vlay.addWidget(self.slider, 0, QtCore.Qt.AlignHCenter)
        vlay.addWidget(self.lineedit, 0, QtCore.Qt.AlignHCenter)

        hlay.addLayout(vlay)
        spacer_item = QtWidgets.QSpacerItem(
            40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
        )
        hlay.addItem(spacer_item)


class MyMainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MyMainWindow, self).__init__()
        uic.loadUi("test2.ui", self)
        self.show()


def main():
    app = QtWidgets.QApplication(sys.argv)
    window = MyMainWindow()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

test2.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>436</width>
    <height>354</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout">
    <item row="0" column="0">
     <widget class="QSplitter" name="splitter">
      <property name="orientation">
       <enum>Qt::Horizontal</enum>
      </property>
      <widget class="QFrame" name="frame">
       <property name="frameShape">
        <enum>QFrame::StyledPanel</enum>
       </property>
       <property name="frameShadow">
        <enum>QFrame::Sunken</enum>
       </property>
       <layout class="QGridLayout" name="gridLayout_2">
        <item row="0" column="0">
         <layout class="QVBoxLayout" name="verticalLayout">
          <item>
           <widget class="QLabel" name="label">
            <property name="text">
             <string>Want this:</string>
            </property>
           </widget>
          </item>
          <item>
           <layout class="QHBoxLayout" name="horizontalLayout_2">
            <item>
             <layout class="QVBoxLayout" name="verticalLayout_3">
              <item alignment="Qt::AlignHCenter">
               <widget class="QSlider" name="verticalSlider">
                <property name="orientation">
                 <enum>Qt::Vertical</enum>
                </property>
               </widget>
              </item>
              <item alignment="Qt::AlignHCenter">
               <widget class="QLineEdit" name="lineEdit">
                <property name="maximumSize">
                 <size>
                  <width>50</width>
                  <height>16777215</height>
                 </size>
                </property>
               </widget>
              </item>
             </layout>
            </item>
            <item>
             <spacer name="horizontalSpacer">
              <property name="orientation">
               <enum>Qt::Horizontal</enum>
              </property>
              <property name="sizeHint" stdset="0">
               <size>
                <width>40</width>
                <height>20</height>
               </size>
              </property>
             </spacer>
            </item>
           </layout>
          </item>
         </layout>
        </item>
       </layout>
      </widget>
      <widget class="QFrame" name="frame_2">
       <property name="frameShape">
        <enum>QFrame::StyledPanel</enum>
       </property>
       <property name="frameShadow">
        <enum>QFrame::Sunken</enum>
       </property>
       <layout class="QGridLayout" name="gridLayout_3">
        <item row="0" column="0">
         <layout class="QVBoxLayout" name="verticalLayout_2">
          <item>
           <widget class="QLabel" name="label_2">
            <property name="text">
             <string>... by promoting these QSliders in QtDesigner:</string>
            </property>
           </widget>
          </item>
          <item>
           <layout class="QHBoxLayout" name="horizontalLayout">
            <item>
             <widget class="CustomWidget" name="widget_2" native="true">
              <layout class="QHBoxLayout" name="horizontalLayout_3">
               <item>
                <widget class="QSlider" name="verticalSlider_2">
                 <property name="orientation">
                  <enum>Qt::Vertical</enum>
                 </property>
                </widget>
               </item>
              </layout>
             </widget>
            </item>
            <item>
             <widget class="CustomWidget" name="widget" native="true">
              <layout class="QHBoxLayout" name="horizontalLayout_4">
               <item>
                <widget class="QSlider" name="verticalSlider_3">
                 <property name="orientation">
                  <enum>Qt::Vertical</enum>
                 </property>
                </widget>
               </item>
              </layout>
             </widget>
            </item>
           </layout>
          </item>
         </layout>
        </item>
       </layout>
      </widget>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>436</width>
     <height>21</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <customwidgets>
  <customwidget>
   <class>CustomWidget</class>
   <extends>QWidget</extends>
   <header>test2</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

当你 运行 test2.py:

时的终端打印输出
$ python3 test2.py
__init__: None []
CustomWidget::
OBJECT CustomWidget::unnamed
  SIGNALS OUT
        <None>
  SIGNALS IN
        <None>
__init__: None []
CustomWidget::
OBJECT CustomWidget::unnamed
  SIGNALS OUT
        <None>
  SIGNALS IN
        <None>
delayedInit: <PyQt5.QtWidgets.QHBoxLayout object at 0x000000000668cdc0> [<PyQt5.QtWidgets.QHBoxLayout object at 0x000000000668cdc0>, <PyQt5.QtWidgets.QSlider object at 0x000000000668ce50>]
CustomWidget::widget
    QHBoxLayout::horizontalLayout_4
    QSlider::verticalSlider_3
OBJECT CustomWidget::widget
  SIGNALS OUT
        signal: destroyed(QObject*)
          <functor or function pointer>
  SIGNALS IN
        <None>
Not deletingLater <PyQt5.QtWidgets.QHBoxLayout object at 0x000000000668cdc0>
delayedInit: <PyQt5.QtWidgets.QHBoxLayout object at 0x000000000668c940> [<PyQt5.QtWidgets.QHBoxLayout object at 0x000000000668c940>, <PyQt5.QtWidgets.QSlider object at 0x000000000668cca0>]
CustomWidget::widget_2
    QHBoxLayout::horizontalLayout_3
    QSlider::verticalSlider_2
OBJECT CustomWidget::widget_2
  SIGNALS OUT
        signal: destroyed(QObject*)
          <functor or function pointer>
  SIGNALS IN
        <None>
Not deletingLater <PyQt5.QtWidgets.QHBoxLayout object at 0x000000000668c940>