如何解决PyQt5 QTabBar paintEvent具有可以移动的选项卡
我想在QTabBar
方法中使用paintEvent(self,event)
进行自定义绘画,同时保持移动选项卡的动画/机制。前几天,我发布了一个类似问题的问题,但措辞不太好,因此我使用以下代码大大简化了该问题:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtTest import QTest
import sys
class MainWindow(QMainWindow):
def __init__(self,parent=None,*args,**kwargs):
QMainWindow.__init__(self,parent,**kwargs)
self.tabs = QTabWidget(self)
self.tabs.setTabBar(TabBar(self.tabs))
self.tabs.setMovable(True)
for color in ["red","orange","yellow","lime","green","cyan","blue","purple","violet","magenta"]:
title = color
widget = QWidget(styleSheet="background-color:%s" % color)
pixmap = Qpixmap(8,8)
pixmap.fill(QColor(color))
icon = QIcon(pixmap)
self.tabs.addTab(widget,icon,title)
self.setCentralWidget(self.tabs)
self.showMaximized()
class TabBar(QTabBar):
def __init__(self,**kwargs):
QTabBar.__init__(self,**kwargs)
def paintEvent(self,event):
painter = qstylePainter(self)
option = qstyleOptionTab()
for i in range(self.count()):
self.initStyleOption(option,i)
#Customise 'option' here
painter.drawControl(qstyle.CE_TabBarTab,option)
def tabSizeHint(self,index):
return QSize(112,48)
def exceptHook(e,v,t):
sys.__excepthook__(e,t)
if __name__ == "__main__":
sys.excepthook = exceptHook
application = QApplication(sys.argv)
mainwindow = MainWindow()
application.exec_()
有一些明显的问题:
- 拖动标签以使其在
QTabBar
中“滑动”是不平滑的(它不会滑动)-它会跳至下一个索引。 - 背景标签(未选中的标签)一旦移位就不会滑入到位-它们会跳入位置。
- 在当前选项卡滑动到标签栏的末端(过去最右边的选项卡),然后放手不能滑回最后一个索引 - 它跳到那里
- 滑动选项卡时,它会同时停留在其原始位置和鼠标光标处(在拖动位置),并且只有释放鼠标时,选项卡 only 才会显示在正确的位置(在此之前,它还会显示在其原始索引处)。
如何在保持标签的所有移动机制/动画的同时,用QTabBar
修改qstyleOptionTab
的绘画?
解决方法
QTabBar看起来似乎有点简单,但至少要提供其所有功能,QTabBar并不是。
如果仔细查看其源代码,您会发现在mouseMoveEvent()
内,只要拖动距离足够宽,就会创建一个 private QMovableTabWidget。该QWidget是QTabBar的子级,它使用选项卡样式选项并跟随鼠标移动来显示“移动”选项卡的QPixmap grab ,同时该选项卡变得不可见。
虽然您的实现看似合理(请注意,我也指的是您的original,now deleted,question),但仍有一些重要问题:
- 这并不能说明上面的“移动”子窗口小部件(事实上,即使您的代码中的 移动窗口小部件实际上并没有移动,但通过您的代码,我仍然可以看到原始选项卡不会调用
mouseMoveEvent()
的基本实现); - 实际上不是 标签;
- 它无法正确处理鼠标事件;
这是一个完整的实现,部分基于C ++源(即使使用垂直制表符,我也对其进行了测试,并且似乎表现出应有的表现):
class TabBar(QTabBar):
class MovingTab(QWidget):
'''
A private QWidget that paints the current moving tab
'''
def setPixmap(self,pixmap):
self.pixmap = pixmap
self.update()
def paintEvent(self,event):
qp = QPainter(self)
qp.drawPixmap(0,self.pixmap)
def __init__(self,parent,*args,**kwargs):
QTabBar.__init__(self,**kwargs)
self.movingTab = None
self.isMoving = False
self.animations = {}
self.pressedIndex = -1
def isVertical(self):
return self.shape() in (
self.RoundedWest,self.RoundedEast,self.TriangularWest,self.TriangularEast)
def createAnimation(self,start,stop):
animation = QVariantAnimation()
animation.setStartValue(start)
animation.setEndValue(stop)
animation.setEasingCurve(QEasingCurve.InOutQuad)
def removeAni():
for k,v in self.animations.items():
if v == animation:
self.animations.pop(k)
animation.deleteLater()
break
animation.finished.connect(removeAni)
animation.valueChanged.connect(self.update)
animation.start()
return animation
def layoutTab(self,overIndex):
oldIndex = self.pressedIndex
self.pressedIndex = overIndex
if overIndex in self.animations:
# if the animation exists,move its key to the swapped index value
self.animations[oldIndex] = self.animations.pop(overIndex)
else:
start = self.tabRect(overIndex).topLeft()
stop = self.tabRect(oldIndex).topLeft()
self.animations[oldIndex] = self.createAnimation(start,stop)
self.moveTab(oldIndex,overIndex)
def finishedMovingTab(self):
self.movingTab.deleteLater()
self.movingTab = None
self.pressedIndex = -1
self.update()
# reimplemented functions
def tabSizeHint(self,i):
return QSize(112,48)
def mousePressEvent(self,event):
super().mousePressEvent(event)
if event.button() == Qt.LeftButton:
self.pressedIndex = self.tabAt(event.pos())
if self.pressedIndex < 0:
return
self.startPos = event.pos()
def mouseMoveEvent(self,event):
if not event.buttons() & Qt.LeftButton or self.pressedIndex < 0:
super().mouseMoveEvent(event)
else:
delta = event.pos() - self.startPos
if not self.isMoving and delta.manhattanLength() < QApplication.startDragDistance():
# ignore the movement as it's too small to be considered a drag
return
if not self.movingTab:
# create a private widget that appears as the current (moving) tab
tabRect = self.tabRect(self.pressedIndex)
overlap = self.style().pixelMetric(
QStyle.PM_TabBarTabOverlap,None,self)
tabRect.adjust(-overlap,overlap,0)
pm = QPixmap(tabRect.size())
pm.fill(Qt.transparent)
qp = QStylePainter(pm,self)
opt = QStyleOptionTab()
self.initStyleOption(opt,self.pressedIndex)
if self.isVertical():
opt.rect.moveTopLeft(QPoint(0,overlap))
else:
opt.rect.moveTopLeft(QPoint(overlap,0))
opt.position = opt.OnlyOneTab
qp.drawControl(QStyle.CE_TabBarTab,opt)
qp.end()
self.movingTab = self.MovingTab(self)
self.movingTab.setPixmap(pm)
self.movingTab.setGeometry(tabRect)
self.movingTab.show()
self.isMoving = True
self.startPos = event.pos()
isVertical = self.isVertical()
startRect = self.tabRect(self.pressedIndex)
if isVertical:
delta = delta.y()
translate = QPoint(0,delta)
startRect.moveTop(startRect.y() + delta)
else:
delta = delta.x()
translate = QPoint(delta,0)
startRect.moveLeft(startRect.x() + delta)
movingRect = self.movingTab.geometry()
movingRect.translate(translate)
self.movingTab.setGeometry(movingRect)
if delta < 0:
overIndex = self.tabAt(startRect.topLeft())
else:
if isVertical:
overIndex = self.tabAt(startRect.bottomLeft())
else:
overIndex = self.tabAt(startRect.topRight())
if overIndex < 0:
return
# if the target tab is valid,move the current whenever its position
# is over the half of its size
overRect = self.tabRect(overIndex)
if isVertical:
if ((overIndex < self.pressedIndex and movingRect.top() < overRect.center().y()) or
(overIndex > self.pressedIndex and movingRect.bottom() > overRect.center().y())):
self.layoutTab(overIndex)
elif ((overIndex < self.pressedIndex and movingRect.left() < overRect.center().x()) or
(overIndex > self.pressedIndex and movingRect.right() > overRect.center().x())):
self.layoutTab(overIndex)
def mouseReleaseEvent(self,event):
super().mouseReleaseEvent(event)
if self.movingTab:
if self.pressedIndex > 0:
animation = self.createAnimation(
self.movingTab.geometry().topLeft(),self.tabRect(self.pressedIndex).topLeft()
)
# restore the position faster than the default 250ms
animation.setDuration(80)
animation.finished.connect(self.finishedMovingTab)
animation.valueChanged.connect(self.movingTab.move)
else:
self.finishedMovingTab()
else:
self.pressedIndex = -1
self.isMoving = False
self.update()
def paintEvent(self,event):
if self.pressedIndex < 0:
super().paintEvent(event)
return
painter = QStylePainter(self)
tabOption = QStyleOptionTab()
for i in range(self.count()):
if i == self.pressedIndex and self.isMoving:
continue
self.initStyleOption(tabOption,i)
if i in self.animations:
tabOption.rect.moveTopLeft(self.animations[i].currentValue())
painter.drawControl(QStyle.CE_TabBarTab,tabOption)
我强烈,建议您仔细阅读并尝试理解上述代码(以及source code),因为我没有评论所有内容完成,如果您将来真的需要进行进一步的子类化,那么了解发生的情况非常重要。
更新
如果在移动 时需要更改其拖动标签的外观,则需要更新其像素图。您可以在创建QStyleOptionTab时存储它,然后在必要时进行更新。在下面的示例中,每更改选项卡的索引时,WindowText
(请注意QPalette.Foreground
已过时)的颜色都会更改:
def mouseMoveEvent(self,event):
# ...
if not self.movingTab:
# ...
self.movingOption = opt
def layoutTab(self,overIndex):
# ...
self.moveTab(oldIndex,overIndex)
pm = QPixmap(self.movingTab.pixmap.size())
pm.fill(Qt.transparent)
qp = QStylePainter(pm,self)
self.movingOption.palette.setColor(QPalette.WindowText,<someColor>)
qp.drawControl(QStyle.CE_TabBarTab,self.movingOption)
qp.end()
self.movingTab.setPixmap(pm)
另一个小建议:虽然您显然可以使用自己喜欢的缩进样式,但是在诸如StackOverflow之类的公共空间上共享代码时,最好还是遵循通用约定,因此我建议您始终为代码提供4 -空间缩进;另外,请记住,每个逗号分隔的变量后都应有一个空格,因为它可以大大提高可读性。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。