-2

我想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,*args,**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,parent,*args,**kwargs):
    QTabBar.__init__(self,parent,*args,**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,v,t)

if __name__ == "__main__":
  sys.excepthook = exceptHook
  application = QApplication(sys.argv)
  mainwindow = MainWindow()
  application.exec_()

有一些明显的问题:

  • 拖动选项卡以“滑动”它QTabBar并不平滑(它不会滑动) - 它会跳转到下一个索引。
  • 背景选项卡(未选择的选项卡)一旦移位就不会滑入到位 - 它们会跳到位。
  • 当标签滑到选项卡栏的末端时(经过最右边的标签),然后放开它,然后将其滑动回到最后一个索引 - 它跳到那里。
  • 滑动选项卡时,它同时停留在原来的位置和鼠标光标处(在它的拖动位置),只有当鼠标松开时,选项卡才会显示在正确的位置(直到那时它也是显示在它最初来自的索引处)。

如何在保留选项卡的所有移动机制/动画的同时QTabBar修改a 的绘画?QStyleOptionTab

4

1 回答 1

1

虽然它可能看起来是一个稍微简单的小部件,但 QTabBar 不是,至少如果您想提供它的所有功能。

如果您仔细查看它的源代码,您会发现只要拖动距离足够宽,就会在私有 QMovableTabWidget 中mouseMoveEvent()创建该 QWidget 是 QTabBar 的子项,它使用选项卡样式选项并跟随鼠标移动显示“移动”选项卡的 QPixmap抓取,同时该选项卡变得不可见。

虽然您的实现可能看起来很合理(请注意,我还指的是您的原始问题,现已删除),但存在一些重要问题:

  • 它不考虑上述“移动”子小部件(实际上,使用您的代码,我仍然可以看到原始选项卡,即使那是实际上没有移动的移动小部件,因为没有调用基本实现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, 0, self.pixmap)

    def __init__(self,parent, *args, **kwargs):
        QTabBar.__init__(self,parent, *args, **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, 0, 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)

强烈建议你仔细阅读并尝试理解上面的代码(连同 代码),因为我没有评论我所做的一切,如果你真的需要做进一步的子类化,了解正在发生的事情非常重要在将来。

更新

如果您需要移动时更改拖动选项卡的外观,则需要更新其像素图。您可以在创建 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 个空格的缩进;另外,请记住,每个逗号分隔的变量后面都应该有一个空格,因为它显着提高了可读性。

于 2020-10-14T03:14:19.120 回答