QSplitter、QWidget 调整大小、setSizes()、setStretchFactor() 和 sizeHint() - 如何让它们协同工作?
QSplitter, QWidget resizing, setSizes(), setStretchFactor(), and sizeHint() - how to make it all work together?
我正在努力研究如何让标题中的所有内容在特定情况下协同工作。我在这里使用 PyQt5,但请随意使用常规 C++ Qt 进行响应,因为我可以很容易地进行翻译。
我正在尝试使用以下内容制作 UI:
一个主窗体(继承自QWidget
,也可以使用QMainWindow
)
主窗体应包含垂直方向的 QSplitter
,顶部包含 QTextEdit
,并包含自定义 class(继承自 QLabel
) 以显示占据剩余 space.
的图像
顶部的 QTextEdit
应该默认为大约 3 行文本高,但是应该可以通过 QSplitter
.[=22= 将其调整到任何合理的极限大小]
自定义 class 应在保持纵横比不变的情况下尽可能调整图像的大小 space。
当然,棘手的部分是根据用户的显示器大小以及表单的移动方式正确调整所有内容的大小。我需要这个 运行 在小至 1,000 px 宽度甚至大至 3,000+ px 宽度的屏幕上。
这是我目前的情况:
# QSplitter3.py
import cv2
import numpy as np
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QSizePolicy, \
QFrame, QTabWidget, QTextEdit, QSplitter
from PyQt5.QtGui import QImage, QPixmap, QPainter
from PyQt5.Qt import Qt
from PyQt5.Qt import QPoint
def main():
app = QApplication([])
screenSize = app.primaryScreen().size()
print('screenSize = ' + str(screenSize.width()) + ', ' + str(screenSize.height()))
mainForm = MainForm(screenSize)
mainForm.show()
app.exec()
# end function
class MainForm(QWidget):
def __init__(self, screenSize):
super().__init__()
# set the title and size of the Qt QWidget window
self.setWindowTitle('Qt Window')
self.setGeometry(screenSize.width() * 0.2, screenSize.height() * 0.2,
screenSize.width() * 0.5 , screenSize.height() * 0.7)
# declare a QTextEdit to show user messages at the top, set the font size, height, and read only property
self.txtUserMessages = QTextEdit()
self.setFontSize(self.txtUserMessages, 14)
self.txtUserMessages.setReadOnly(True)
# make the min height of the text box about 2 lines of text high
self.txtUserMessages.setMinimumHeight(self.getTextEditHeightForNLines(self.txtUserMessages, 2))
# populate the user messages text box with some example text
self.txtUserMessages.append('message 1')
self.txtUserMessages.append('message 2')
self.txtUserMessages.append('message 3')
self.txtUserMessages.append('stuff here')
self.txtUserMessages.append('bla bla bla')
self.txtUserMessages.append('asdasdsadds')
# instantiate the custom ImageWidget class below to show the image
self.imageWidget = ImageWidget()
self.imageWidget.setMargin(0)
self.imageWidget.setContentsMargins(0, 0, 0, 0)
self.imageWidget.setScaledContents(True)
self.imageWidget.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.imageWidget.setAlignment(Qt.AlignCenter)
# declare the splitter, then add the user message text box and tab widget
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.txtUserMessages)
self.splitter.addWidget(self.imageWidget)
defaultTextEditHeight = self.getTextEditHeightForNLines(self.txtUserMessages, 3)
print('defaultTextEditHeight = ' + str(defaultTextEditHeight))
# How can I use defaultTextEditHeight height here, but still allow resizing ??
# I really don't like this line, the 1000 is a guess and check that may only work with one screen size !!!
self.splitter.setSizes([defaultTextEditHeight, 1000])
# Should setStretchFactor be used here ?? This does not seem to work
# self.splitter.setStretchFactor(0, 0)
# self.splitter.setStretchFactor(1, 1)
# What about sizeHint() ?? Should that be used here, and if so, how ??
# set the main form's layout to the QGridLayout
self.gridLayout = QGridLayout()
self.gridLayout.addWidget(self.splitter)
self.setLayout(self.gridLayout)
# open the two images in OpenCV format
self.openCvImage = cv2.imread('image.jpg')
if self.openCvImage is None:
print('error opening image')
return
# end if
# convert the OpenCV image to QImage
self.qtImage = openCvImageToQImage(self.openCvImage)
# show the QImage on the ImageWidget
self.imageWidget.setPixmap(QPixmap.fromImage(self.qtImage))
# end function
def setFontSize(self, widget, fontSize):
font = widget.font()
font.setPointSize(fontSize)
widget.setFont(font)
# end function
def getTextEditHeightForNLines(self, textEdit, numLines):
fontMetrics = textEdit.fontMetrics()
rowHeight = fontMetrics.lineSpacing()
rowHeight = rowHeight * 1.21
textEditHeight = int(numLines * rowHeight)
return textEditHeight
# end function
# end class
def openCvImageToQImage(openCvImage):
# get the height, width, and num channels of the OpenCV image, then compute the byte value
height, width, numChannels = openCvImage.shape
byteValue = numChannels * width
# make the QImage from the OpenCV image
qtImage = QImage(openCvImage.data, width, height, byteValue, QImage.Format_RGB888).rgbSwapped()
return qtImage
# end function
class ImageWidget(QLabel):
def __init__(self):
super(QLabel, self).__init__()
# end function
def setPixmap(self, pixmap):
self.pixmap = pixmap
# end function
def paintEvent(self, event):
size = self.size()
painter = QPainter(self)
point = QPoint(0, 0)
scaledPixmap = self.pixmap.scaled(size, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
point.setX((size.width() - scaledPixmap.width()) / 2)
point.setY((size.height() - scaledPixmap.height()) / 2)
painter.drawPixmap(point, scaledPixmap)
# end function
# end class
if __name__ == '__main__':
main()
目前我正在 2560x1440 的屏幕上进行测试,并且在输入魔法 1000
后它可以在此屏幕尺寸上运行,但我真的不喜欢 hard-coded 1000
。我怀疑我遗漏的代码区域是这一部分:
# declare the splitter, then add the user message text box and tab widget
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.txtUserMessages)
self.splitter.addWidget(self.imageWidget)
defaultTextEditHeight = self.getTextEditHeightForNLines(self.txtUserMessages, 3)
print('defaultTextEditHeight = ' + str(defaultTextEditHeight))
# How can I use defaultTextEditHeight height here, but still allow resizing ??
# I really don't like this line, the 1000 is a guess and check that may only work with one screen size !!!
self.splitter.setSizes([defaultTextEditHeight, 1000])
# Should setStretchFactor be used here ?? This does not seem to work
# self.splitter.setStretchFactor(0, 0)
# self.splitter.setStretchFactor(1, 1)
# What about sizeHint() ?? Should that be used here, and if so, how ??
# set the main form's layout to the QGridLayout
self.gridLayout = QGridLayout()
self.gridLayout.addWidget(self.splitter)
使用硬编码 1000 并在此特定屏幕上运行良好:
重申(希望更清楚)我正在尝试删除 hard-coded 1000 并按如下方式命令 Qt:
- 最初让表单占据大约 2/3 的屏幕
- 最初使文本框高约 3 行文本(最少 2 行文本高)
- 允许用户使用 QSplitter 随时无限制地调整文本框和图像的大小
- 调整表单大小时(或最小化或最大化),根据用户在调整大小时的使用方式按比例调整文本框和图像的大小
我已经尝试了标题中提到的内容的每一种组合,到目前为止 post 但我无法获得此功能,除了 hard-coded 1000这可能不适用于不同的屏幕尺寸。
如何删除 hard-coded 1000 并修改上面的内容以实现预期的功能?
在我的解决方案中,我不会考虑 opencv 的部分,因为它增加了不必要的复杂性。
解决方案是使用setStretchFactor()
方法,在这种情况下重写QTextEdit
的sizeHint()
方法来设置初始尺寸和setMinimumHeight()来设置最小高度。为了显示图像,我使用 QGraphicsView
而不是 QLabel
,因为逻辑更简单。
from PyQt5 import QtCore, QtGui, QtWidgets
class TextEdit(QtWidgets.QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
font = self.font()
font.setPointSize(14)
self.setFont(font)
self.setMinimumHeight(self.heightForLines(2))
def heightForLines(self, n):
return (
n * self.fontMetrics().lineSpacing() + 2 * self.document().documentMargin()
)
def showEvent(self, event):
self.verticalScrollBar().setValue(self.verticalScrollBar().minimum())
def sizeHint(self):
s = super().sizeHint()
s.setHeight(self.heightForLines(3))
return s
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setBackgroundBrush(self.palette().brush(QtGui.QPalette.Window))
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self._pixmap_item = QtWidgets.QGraphicsPixmapItem()
scene.addItem(self._pixmap_item)
def setPixmap(self, pixmap):
self._pixmap_item.setPixmap(pixmap)
def resizeEvent(self, event):
self.fitInView(self._pixmap_item, QtCore.Qt.KeepAspectRatio)
self.centerOn(self._pixmap_item)
super().resizeEvent(event)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.textedit = TextEdit()
for i in range(10):
self.textedit.append("Message {}".format(i))
self.graphicsview = GraphicsView()
self.graphicsview.setPixmap(QtGui.QPixmap("image.jpg"))
splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
splitter.addWidget(self.textedit)
splitter.addWidget(self.graphicsview)
splitter.setStretchFactor(1, 1)
lay = QtWidgets.QGridLayout(self)
lay.addWidget(splitter)
screenSize = QtWidgets.QApplication.primaryScreen().size()
self.setGeometry(
screenSize.width() * 0.2,
screenSize.height() * 0.2,
screenSize.width() * 0.5,
screenSize.height() * 0.7,
)
def main():
app = QtWidgets.QApplication([])
w = Widget()
w.resize(640, 480)
w.show()
app.exec_()
if __name__ == "__main__":
main()
我正在努力研究如何让标题中的所有内容在特定情况下协同工作。我在这里使用 PyQt5,但请随意使用常规 C++ Qt 进行响应,因为我可以很容易地进行翻译。
我正在尝试使用以下内容制作 UI:
一个主窗体(继承自
QWidget
,也可以使用QMainWindow
)主窗体应包含垂直方向的
QSplitter
,顶部包含QTextEdit
,并包含自定义 class(继承自QLabel
) 以显示占据剩余 space. 的图像
顶部的
QTextEdit
应该默认为大约 3 行文本高,但是应该可以通过QSplitter
.[=22= 将其调整到任何合理的极限大小]自定义 class 应在保持纵横比不变的情况下尽可能调整图像的大小 space。
当然,棘手的部分是根据用户的显示器大小以及表单的移动方式正确调整所有内容的大小。我需要这个 运行 在小至 1,000 px 宽度甚至大至 3,000+ px 宽度的屏幕上。
这是我目前的情况:
# QSplitter3.py
import cv2
import numpy as np
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QSizePolicy, \
QFrame, QTabWidget, QTextEdit, QSplitter
from PyQt5.QtGui import QImage, QPixmap, QPainter
from PyQt5.Qt import Qt
from PyQt5.Qt import QPoint
def main():
app = QApplication([])
screenSize = app.primaryScreen().size()
print('screenSize = ' + str(screenSize.width()) + ', ' + str(screenSize.height()))
mainForm = MainForm(screenSize)
mainForm.show()
app.exec()
# end function
class MainForm(QWidget):
def __init__(self, screenSize):
super().__init__()
# set the title and size of the Qt QWidget window
self.setWindowTitle('Qt Window')
self.setGeometry(screenSize.width() * 0.2, screenSize.height() * 0.2,
screenSize.width() * 0.5 , screenSize.height() * 0.7)
# declare a QTextEdit to show user messages at the top, set the font size, height, and read only property
self.txtUserMessages = QTextEdit()
self.setFontSize(self.txtUserMessages, 14)
self.txtUserMessages.setReadOnly(True)
# make the min height of the text box about 2 lines of text high
self.txtUserMessages.setMinimumHeight(self.getTextEditHeightForNLines(self.txtUserMessages, 2))
# populate the user messages text box with some example text
self.txtUserMessages.append('message 1')
self.txtUserMessages.append('message 2')
self.txtUserMessages.append('message 3')
self.txtUserMessages.append('stuff here')
self.txtUserMessages.append('bla bla bla')
self.txtUserMessages.append('asdasdsadds')
# instantiate the custom ImageWidget class below to show the image
self.imageWidget = ImageWidget()
self.imageWidget.setMargin(0)
self.imageWidget.setContentsMargins(0, 0, 0, 0)
self.imageWidget.setScaledContents(True)
self.imageWidget.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.imageWidget.setAlignment(Qt.AlignCenter)
# declare the splitter, then add the user message text box and tab widget
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.txtUserMessages)
self.splitter.addWidget(self.imageWidget)
defaultTextEditHeight = self.getTextEditHeightForNLines(self.txtUserMessages, 3)
print('defaultTextEditHeight = ' + str(defaultTextEditHeight))
# How can I use defaultTextEditHeight height here, but still allow resizing ??
# I really don't like this line, the 1000 is a guess and check that may only work with one screen size !!!
self.splitter.setSizes([defaultTextEditHeight, 1000])
# Should setStretchFactor be used here ?? This does not seem to work
# self.splitter.setStretchFactor(0, 0)
# self.splitter.setStretchFactor(1, 1)
# What about sizeHint() ?? Should that be used here, and if so, how ??
# set the main form's layout to the QGridLayout
self.gridLayout = QGridLayout()
self.gridLayout.addWidget(self.splitter)
self.setLayout(self.gridLayout)
# open the two images in OpenCV format
self.openCvImage = cv2.imread('image.jpg')
if self.openCvImage is None:
print('error opening image')
return
# end if
# convert the OpenCV image to QImage
self.qtImage = openCvImageToQImage(self.openCvImage)
# show the QImage on the ImageWidget
self.imageWidget.setPixmap(QPixmap.fromImage(self.qtImage))
# end function
def setFontSize(self, widget, fontSize):
font = widget.font()
font.setPointSize(fontSize)
widget.setFont(font)
# end function
def getTextEditHeightForNLines(self, textEdit, numLines):
fontMetrics = textEdit.fontMetrics()
rowHeight = fontMetrics.lineSpacing()
rowHeight = rowHeight * 1.21
textEditHeight = int(numLines * rowHeight)
return textEditHeight
# end function
# end class
def openCvImageToQImage(openCvImage):
# get the height, width, and num channels of the OpenCV image, then compute the byte value
height, width, numChannels = openCvImage.shape
byteValue = numChannels * width
# make the QImage from the OpenCV image
qtImage = QImage(openCvImage.data, width, height, byteValue, QImage.Format_RGB888).rgbSwapped()
return qtImage
# end function
class ImageWidget(QLabel):
def __init__(self):
super(QLabel, self).__init__()
# end function
def setPixmap(self, pixmap):
self.pixmap = pixmap
# end function
def paintEvent(self, event):
size = self.size()
painter = QPainter(self)
point = QPoint(0, 0)
scaledPixmap = self.pixmap.scaled(size, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
point.setX((size.width() - scaledPixmap.width()) / 2)
point.setY((size.height() - scaledPixmap.height()) / 2)
painter.drawPixmap(point, scaledPixmap)
# end function
# end class
if __name__ == '__main__':
main()
目前我正在 2560x1440 的屏幕上进行测试,并且在输入魔法 1000
后它可以在此屏幕尺寸上运行,但我真的不喜欢 hard-coded 1000
。我怀疑我遗漏的代码区域是这一部分:
# declare the splitter, then add the user message text box and tab widget
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.txtUserMessages)
self.splitter.addWidget(self.imageWidget)
defaultTextEditHeight = self.getTextEditHeightForNLines(self.txtUserMessages, 3)
print('defaultTextEditHeight = ' + str(defaultTextEditHeight))
# How can I use defaultTextEditHeight height here, but still allow resizing ??
# I really don't like this line, the 1000 is a guess and check that may only work with one screen size !!!
self.splitter.setSizes([defaultTextEditHeight, 1000])
# Should setStretchFactor be used here ?? This does not seem to work
# self.splitter.setStretchFactor(0, 0)
# self.splitter.setStretchFactor(1, 1)
# What about sizeHint() ?? Should that be used here, and if so, how ??
# set the main form's layout to the QGridLayout
self.gridLayout = QGridLayout()
self.gridLayout.addWidget(self.splitter)
使用硬编码 1000 并在此特定屏幕上运行良好:
重申(希望更清楚)我正在尝试删除 hard-coded 1000 并按如下方式命令 Qt:
- 最初让表单占据大约 2/3 的屏幕
- 最初使文本框高约 3 行文本(最少 2 行文本高)
- 允许用户使用 QSplitter 随时无限制地调整文本框和图像的大小
- 调整表单大小时(或最小化或最大化),根据用户在调整大小时的使用方式按比例调整文本框和图像的大小
我已经尝试了标题中提到的内容的每一种组合,到目前为止 post 但我无法获得此功能,除了 hard-coded 1000这可能不适用于不同的屏幕尺寸。
如何删除 hard-coded 1000 并修改上面的内容以实现预期的功能?
在我的解决方案中,我不会考虑 opencv 的部分,因为它增加了不必要的复杂性。
解决方案是使用setStretchFactor()
方法,在这种情况下重写QTextEdit
的sizeHint()
方法来设置初始尺寸和setMinimumHeight()来设置最小高度。为了显示图像,我使用 QGraphicsView
而不是 QLabel
,因为逻辑更简单。
from PyQt5 import QtCore, QtGui, QtWidgets
class TextEdit(QtWidgets.QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
font = self.font()
font.setPointSize(14)
self.setFont(font)
self.setMinimumHeight(self.heightForLines(2))
def heightForLines(self, n):
return (
n * self.fontMetrics().lineSpacing() + 2 * self.document().documentMargin()
)
def showEvent(self, event):
self.verticalScrollBar().setValue(self.verticalScrollBar().minimum())
def sizeHint(self):
s = super().sizeHint()
s.setHeight(self.heightForLines(3))
return s
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setBackgroundBrush(self.palette().brush(QtGui.QPalette.Window))
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self._pixmap_item = QtWidgets.QGraphicsPixmapItem()
scene.addItem(self._pixmap_item)
def setPixmap(self, pixmap):
self._pixmap_item.setPixmap(pixmap)
def resizeEvent(self, event):
self.fitInView(self._pixmap_item, QtCore.Qt.KeepAspectRatio)
self.centerOn(self._pixmap_item)
super().resizeEvent(event)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.textedit = TextEdit()
for i in range(10):
self.textedit.append("Message {}".format(i))
self.graphicsview = GraphicsView()
self.graphicsview.setPixmap(QtGui.QPixmap("image.jpg"))
splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
splitter.addWidget(self.textedit)
splitter.addWidget(self.graphicsview)
splitter.setStretchFactor(1, 1)
lay = QtWidgets.QGridLayout(self)
lay.addWidget(splitter)
screenSize = QtWidgets.QApplication.primaryScreen().size()
self.setGeometry(
screenSize.width() * 0.2,
screenSize.height() * 0.2,
screenSize.width() * 0.5,
screenSize.height() * 0.7,
)
def main():
app = QtWidgets.QApplication([])
w = Widget()
w.resize(640, 480)
w.show()
app.exec_()
if __name__ == "__main__":
main()