微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

PyQt5 QTabBar paintEvent具有可以移动的选项卡

如何解决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 举报,一经查实,本站将立刻删除。