QScrollArea:从一个项目滚动到另一个项目
QScrollArea: Scroll from item to item
请考虑以下代码:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Gallery(QScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedWidth(175)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# Set widget and layout
self._scroll_widget = QWidget()
self._layout = QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.setSpacing(25)
self._scroll_widget.setLayout(self._layout)
self.setWidget(self._scroll_widget)
self.setWidgetResizable(True)
# Stretch
self._layout.addStretch(1) # Stretch above widgets
self._layout.addStretch(1) # Stretch below widgets
# Initialize ---------------------------------|
for _ in range(10):
self.add_item()
def resizeEvent(self, event: QResizeEvent) -> None:
super().resizeEvent(event)
# Calculate Margins --------------------|
children = self._scroll_widget.findChildren(QLabel)
first_widget = children[0]
last_widget = children[-1]
self._layout.setContentsMargins(
0,
int(event.size().height() / 2 - first_widget.size().height() / 2),
0,
int(event.size().height() / 2 - last_widget.size().height() / 2)
)
def add_item(self) -> None:
widget = QLabel()
widget.setStyleSheet('background: #22FF88')
widget.setFixedSize(90, 125)
child_count = len(
self._scroll_widget.findChildren(QLabel)
)
self._layout.insertWidget(1 + child_count, widget,
alignment=Qt.AlignCenter)
if __name__ == '__main__':
app = QApplication([])
window = Gallery()
window.show()
app.exec()
目前,布局的边距是动态设置的,因此,无论 window 的大小如何,第一个和最后一个项目始终垂直居中:
我现在想要实现的是,每当我滚动(通过鼠标滚轮或箭头键,因为滚动条被禁用)时,下一个小部件应该位于垂直中心,即我想切换滚动模式从 per pixel 到 per-widget-basis,这样无论我滚动多远,我都不会着陆 在 两个小部件之间。
如何做到这一点?
我发现 QAbstractItemView provides the option the switch the ScrollMode 到 ScrollPerItem,但我不确定这是否是我需要的,因为我在尝试子类化时有点不知所措 QAbstractItemView.
编辑:
这显示了我在适应 @musicamante 的回答后注意到的延迟:
这并不是真正的干扰,但我在较大的项目中没有看到它,所以我想有些地方没有正常工作。
由于 QScrollArea 提供的大多数功能实际上将被忽略,因此从它继承子类并没有带来很多好处。相反,它会使事情变得更加复杂。
此外,使用布局也不是很有用:小部件的“容器”不受滚动区域的限制,所有大小提示和调整大小的功能在这种情况下几乎无用。
解决方案可能是将所有项目设置为“滚动区域”的子项,甚至可以是基本的 QWidget 或 QFrame,但为了更好的样式支持,我选择使用 QAbstractScrollArea。
诀窍是根据其几何形状计算每个子部件的正确位置。请注意,我假设所有小部件都具有固定大小,否则您可能需要使用它们的 sizeHint、minimumSizeHint 或最小尺寸,并检查它们的尺寸策略。
这是一个可能的实现(我更改了项目创建以正确显示结果):
from random import randrange
class Gallery(QAbstractScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedWidth(175)
self.items = []
self.currentIndex = -1
for _ in range(10):
widget = QLabel(str(len(self.items) + 1),
self, alignment=Qt.AlignCenter)
widget.setStyleSheet('background: #{:02x}{:02x}{:02x}'.format(
randrange(255), randrange(255), randrange(255)))
widget.setFixedSize(randrange(60, 100), randrange(50, 200))
self.addItem(widget)
def addItem(self, widget):
self.insertItem(len(self.items), widget)
def insertItem(self, index, widget):
widget.setParent(self.viewport())
widget.show()
self.items.insert(index, widget)
if len(self.items) == 1:
self.currentIndex = 0
self.updateGeometry()
def setCurrentIndex(self, index):
if not self.items:
self.currentIndex = -1
return
self.currentIndex = max(0, min(index, len(self.items) - 1))
self.updateGeometry()
def stepBy(self, step):
self.setCurrentIndex(self.currentIndex + step)
def updateGeometry(self):
super().updateGeometry()
if not self.items:
return
rects = []
y = 0
for i, widget in enumerate(self.items):
rect = widget.rect()
rect.moveTop(y)
rects.append(rect)
if i == self.currentIndex:
centerY = rect.center().y()
y = rect.bottom() + 25
centerY -= self.height() / 2
centerX = self.width() / 2
for widget, rect in zip(self.items, rects):
widget.setGeometry(rect.translated(centerX - rect.width() / 2, -centerY))
def sizeHint(self):
return QSize(175, 400)
def resizeEvent(self, event: QResizeEvent):
self.updateGeometry()
def wheelEvent(self, event):
if event.angleDelta().y() < 0:
self.stepBy(1)
else:
self.stepBy(-1)
请考虑以下代码:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Gallery(QScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedWidth(175)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# Set widget and layout
self._scroll_widget = QWidget()
self._layout = QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.setSpacing(25)
self._scroll_widget.setLayout(self._layout)
self.setWidget(self._scroll_widget)
self.setWidgetResizable(True)
# Stretch
self._layout.addStretch(1) # Stretch above widgets
self._layout.addStretch(1) # Stretch below widgets
# Initialize ---------------------------------|
for _ in range(10):
self.add_item()
def resizeEvent(self, event: QResizeEvent) -> None:
super().resizeEvent(event)
# Calculate Margins --------------------|
children = self._scroll_widget.findChildren(QLabel)
first_widget = children[0]
last_widget = children[-1]
self._layout.setContentsMargins(
0,
int(event.size().height() / 2 - first_widget.size().height() / 2),
0,
int(event.size().height() / 2 - last_widget.size().height() / 2)
)
def add_item(self) -> None:
widget = QLabel()
widget.setStyleSheet('background: #22FF88')
widget.setFixedSize(90, 125)
child_count = len(
self._scroll_widget.findChildren(QLabel)
)
self._layout.insertWidget(1 + child_count, widget,
alignment=Qt.AlignCenter)
if __name__ == '__main__':
app = QApplication([])
window = Gallery()
window.show()
app.exec()
目前,布局的边距是动态设置的,因此,无论 window 的大小如何,第一个和最后一个项目始终垂直居中:
我现在想要实现的是,每当我滚动(通过鼠标滚轮或箭头键,因为滚动条被禁用)时,下一个小部件应该位于垂直中心,即我想切换滚动模式从 per pixel 到 per-widget-basis,这样无论我滚动多远,我都不会着陆 在 两个小部件之间。
如何做到这一点?
我发现 QAbstractItemView provides the option the switch the ScrollMode 到 ScrollPerItem,但我不确定这是否是我需要的,因为我在尝试子类化时有点不知所措 QAbstractItemView.
编辑:
这显示了我在适应 @musicamante 的回答后注意到的延迟:
这并不是真正的干扰,但我在较大的项目中没有看到它,所以我想有些地方没有正常工作。
由于 QScrollArea 提供的大多数功能实际上将被忽略,因此从它继承子类并没有带来很多好处。相反,它会使事情变得更加复杂。
此外,使用布局也不是很有用:小部件的“容器”不受滚动区域的限制,所有大小提示和调整大小的功能在这种情况下几乎无用。
解决方案可能是将所有项目设置为“滚动区域”的子项,甚至可以是基本的 QWidget 或 QFrame,但为了更好的样式支持,我选择使用 QAbstractScrollArea。
诀窍是根据其几何形状计算每个子部件的正确位置。请注意,我假设所有小部件都具有固定大小,否则您可能需要使用它们的 sizeHint、minimumSizeHint 或最小尺寸,并检查它们的尺寸策略。
这是一个可能的实现(我更改了项目创建以正确显示结果):
from random import randrange
class Gallery(QAbstractScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedWidth(175)
self.items = []
self.currentIndex = -1
for _ in range(10):
widget = QLabel(str(len(self.items) + 1),
self, alignment=Qt.AlignCenter)
widget.setStyleSheet('background: #{:02x}{:02x}{:02x}'.format(
randrange(255), randrange(255), randrange(255)))
widget.setFixedSize(randrange(60, 100), randrange(50, 200))
self.addItem(widget)
def addItem(self, widget):
self.insertItem(len(self.items), widget)
def insertItem(self, index, widget):
widget.setParent(self.viewport())
widget.show()
self.items.insert(index, widget)
if len(self.items) == 1:
self.currentIndex = 0
self.updateGeometry()
def setCurrentIndex(self, index):
if not self.items:
self.currentIndex = -1
return
self.currentIndex = max(0, min(index, len(self.items) - 1))
self.updateGeometry()
def stepBy(self, step):
self.setCurrentIndex(self.currentIndex + step)
def updateGeometry(self):
super().updateGeometry()
if not self.items:
return
rects = []
y = 0
for i, widget in enumerate(self.items):
rect = widget.rect()
rect.moveTop(y)
rects.append(rect)
if i == self.currentIndex:
centerY = rect.center().y()
y = rect.bottom() + 25
centerY -= self.height() / 2
centerX = self.width() / 2
for widget, rect in zip(self.items, rects):
widget.setGeometry(rect.translated(centerX - rect.width() / 2, -centerY))
def sizeHint(self):
return QSize(175, 400)
def resizeEvent(self, event: QResizeEvent):
self.updateGeometry()
def wheelEvent(self, event):
if event.angleDelta().y() < 0:
self.stepBy(1)
else:
self.stepBy(-1)