【问题标题】:In PyQt, is it possible to detach tabs from a QTabWidget?在 PyQt 中,是否可以从 QTabWidget 中分离选项卡?
【发布时间】:2018-04-26 07:18:22
【问题描述】:

许多专业应用程序(例如网络浏览器)让用户能够从标签栏分离标签。令人惊讶的是,Qt4 不提供此功能。有人可能会说这个功能是通过使用 tabified QDockWidgets 提供的。然而,也有人认为 QDockWidgets 的实现让用户看起来不专业且不直观。

【问题讨论】:

    标签: python qt pyqt pyqt4 qtabwidget


    【解决方案1】:

    原始解决方案由本主题的作者 Blackwood 在下面发布,所有功劳归他们所有

    谢谢你。我正在寻找这个并且发生在这个线程上。

    我正在寻找的应用程序使用 PyQt5,虽然相似,但有足够的差异来破坏上面发布的代码。

    我编辑了代码以使其适用于 PyQt5。它可以工作,但没有经过适当的错误测试

    from PyQt5 import QtGui, QtCore,QtWidgets
    from PyQt5.QtCore import pyqtSignal, pyqtSlot
    ##
    # The DetachableTabWidget adds additional functionality to Qt's QTabWidget that allows it
    # to detach and re-attach tabs.
    #
    # Additional Features:
    #   Detach tabs by
    #     dragging the tabs away from the tab bar
    #     double clicking the tab
    #   Re-attach tabs by
    #     dragging the detached tab's window into the tab bar
    #     closing the detached tab's window
    #   Remove tab (attached or detached) by name
    #
    # Modified Features:
    #   Re-ordering (moving) tabs by dragging was re-implemented
    #
    #   Original by Stack Overflow user: Blackwood, 13/11/2017
    #
    #   Adapted for PyQt5 
    #
    class DetachableTabWidget(QtWidgets.QTabWidget):
        def __init__(self, parent=None):
    
            super().__init__()
    
            self.tabBar = self.TabBar(self)
            self.tabBar.onDetachTabSignal.connect(self.detachTab)
            self.tabBar.onMoveTabSignal.connect(self.moveTab)
            self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop)
    
            self.setTabBar(self.tabBar)
    
            # Used to keep a reference to detached tabs since their QMainWindow
            # does not have a parent
            self.detachedTabs = {}
    
            # Close all detached tabs if the application is closed explicitly
            QtWidgets.qApp.aboutToQuit.connect(self.closeDetachedTabs) # @UndefinedVariable
    
    
        ##
        #  The default movable functionality of QTabWidget must remain disabled
        #  so as not to conflict with the added features
        def setMovable(self, movable):
            pass
    
        ##
        #  Move a tab from one position (index) to another
        #
        #  @param    fromIndex    the original index location of the tab
        #  @param    toIndex      the new index location of the tab
        @pyqtSlot(int, int)
        def moveTab(self, fromIndex, toIndex):
            widget = self.widget(fromIndex)
            icon = self.tabIcon(fromIndex)
            text = self.tabText(fromIndex)
    
            self.removeTab(fromIndex)
            self.insertTab(toIndex, widget, icon, text)
            self.setCurrentIndex(toIndex)
    
    
        ##
        #  Detach the tab by removing it's contents and placing them in
        #  a DetachedTab window
        #
        #  @param    index    the index location of the tab to be detached
        #  @param    point    the screen position for creating the new DetachedTab window
        @pyqtSlot(int, QtCore.QPoint)
        def detachTab(self, index, point):
    
            # Get the tab content
            name = self.tabText(index)
            icon = self.tabIcon(index)
            if icon.isNull():
                icon = self.window().windowIcon()
            contentWidget = self.widget(index)
    
            try:
                contentWidgetRect = contentWidget.frameGeometry()
            except AttributeError:
                return
    
            # Create a new detached tab window
            detachedTab = self.DetachedTab(name, contentWidget)
            detachedTab.setWindowModality(QtCore.Qt.NonModal)
            detachedTab.setWindowIcon(icon)
            detachedTab.setGeometry(contentWidgetRect)
            detachedTab.onCloseSignal.connect(self.attachTab)
            detachedTab.onDropSignal.connect(self.tabBar.detachedTabDrop)
            detachedTab.move(point)
            detachedTab.show()
    
    
            # Create a reference to maintain access to the detached tab
            self.detachedTabs[name] = detachedTab
    
    
        ##
        #  Re-attach the tab by removing the content from the DetachedTab window,
        #  closing it, and placing the content back into the DetachableTabWidget
        #
        #  @param    contentWidget    the content widget from the DetachedTab window
        #  @param    name             the name of the detached tab
        #  @param    icon             the window icon for the detached tab
        #  @param    insertAt         insert the re-attached tab at the given index
        def attachTab(self, contentWidget, name, icon, insertAt=None):
    
            # Make the content widget a child of this widget
            contentWidget.setParent(self)
    
    
            # Remove the reference
            del self.detachedTabs[name]
    
    
            # Create an image from the given icon (for comparison)
            if not icon.isNull():
                try:
                    tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
                    tabIconImage = tabIconPixmap.toImage()
                except IndexError:
                    tabIconImage = None
            else:
                tabIconImage = None
    
    
            # Create an image of the main window icon (for comparison)
            if not icon.isNull():
                try:
                    windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
                    windowIconImage = windowIconPixmap.toImage()
                except IndexError:
                    windowIconImage = None
            else:
                windowIconImage = None
    
    
            # Determine if the given image and the main window icon are the same.
            # If they are, then do not add the icon to the tab
            if tabIconImage == windowIconImage:
                if insertAt == None:
                    index = self.addTab(contentWidget, name)
                else:
                    index = self.insertTab(insertAt, contentWidget, name)
            else:
                if insertAt == None:
                    index = self.addTab(contentWidget, icon, name)
                else:
                    index = self.insertTab(insertAt, contentWidget, icon, name)
    
    
            # Make this tab the current tab
            if index > -1:
                self.setCurrentIndex(index)
    
    
        ##
        #  Remove the tab with the given name, even if it is detached
        #
        #  @param    name    the name of the tab to be removed
        def removeTabByName(self, name):
    
            # Remove the tab if it is attached
            attached = False
            for index in xrange(self.count()):
                if str(name) == str(self.tabText(index)):
                    self.removeTab(index)
                    attached = True
                    break
    
    
            # If the tab is not attached, close it's window and
            # remove the reference to it
            if not attached:
                for key in self.detachedTabs:
                    if str(name) == str(key):
                        self.detachedTabs[key].onCloseSignal.disconnect()
                        self.detachedTabs[key].close()
                        del self.detachedTabs[key]
                        break
    
    
        ##
        #  Handle dropping of a detached tab inside the DetachableTabWidget
        #
        #  @param    name     the name of the detached tab
        #  @param    index    the index of an existing tab (if the tab bar
        #                     determined that the drop occurred on an
        #                     existing tab)
        #  @param    dropPos  the mouse cursor position when the drop occurred
        @QtCore.pyqtSlot(str, int, QtCore.QPoint)
        def detachedTabDrop(self, name, index, dropPos):
    
            # If the drop occurred on an existing tab, insert the detached
            # tab at the existing tab's location
            if index > -1:
    
                # Create references to the detached tab's content and icon
                contentWidget = self.detachedTabs[name].contentWidget
                icon = self.detachedTabs[name].windowIcon()
    
                # Disconnect the detached tab's onCloseSignal so that it
                # does not try to re-attach automatically
                self.detachedTabs[name].onCloseSignal.disconnect()
    
                # Close the detached
                self.detachedTabs[name].close()
    
                # Re-attach the tab at the given index
                self.attachTab(contentWidget, name, icon, index)
    
    
            # If the drop did not occur on an existing tab, determine if the drop
            # occurred in the tab bar area (the area to the side of the QTabBar)
            else:
    
                # Find the drop position relative to the DetachableTabWidget
                tabDropPos = self.mapFromGlobal(dropPos)
    
                # If the drop position is inside the DetachableTabWidget...
                if self.rect().contains(tabDropPos):
    
                    # If the drop position is inside the tab bar area (the
                    # area to the side of the QTabBar) or there are not tabs
                    # currently attached...
                    if tabDropPos.y() < self.tabBar.height() or self.count() == 0:
    
                        # Close the detached tab and allow it to re-attach
                        # automatically
                        self.detachedTabs[name].close()
    
    
        ##
        #  Close all tabs that are currently detached.
        def closeDetachedTabs(self):
            listOfDetachedTabs = []
    
            for key in self.detachedTabs:
                listOfDetachedTabs.append(self.detachedTabs[key])
    
            for detachedTab in listOfDetachedTabs:
                detachedTab.close()
    
    
        ##
        #  When a tab is detached, the contents are placed into this QMainWindow.  The tab
        #  can be re-attached by closing the dialog or by dragging the window into the tab bar
        class DetachedTab(QtWidgets.QMainWindow):
            onCloseSignal = pyqtSignal(QtWidgets.QWidget, str, QtGui.QIcon)
            onDropSignal = pyqtSignal(str, QtCore.QPoint)
    
            def __init__(self, name, contentWidget):
                QtWidgets.QMainWindow.__init__(self, None)
    
                self.setObjectName(name)
                self.setWindowTitle(name)
    
                self.contentWidget = contentWidget
                self.setCentralWidget(self.contentWidget)
                self.contentWidget.show()
    
                self.windowDropFilter = self.WindowDropFilter()
                self.installEventFilter(self.windowDropFilter)
                self.windowDropFilter.onDropSignal.connect(self.windowDropSlot)
    
    
            ##
            #  Handle a window drop event
            #
            #  @param    dropPos    the mouse cursor position of the drop
            @QtCore.pyqtSlot(QtCore.QPoint)
            def windowDropSlot(self, dropPos):
                self.onDropSignal.emit(self.objectName(), dropPos)
    
    
            ##
            #  If the window is closed, emit the onCloseSignal and give the
            #  content widget back to the DetachableTabWidget
            #
            #  @param    event    a close event
            def closeEvent(self, event):
                self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon())
    
    
            ##
            #  An event filter class to detect a QMainWindow drop event
            class WindowDropFilter(QtCore.QObject):
                onDropSignal = pyqtSignal(QtCore.QPoint)
    
                def __init__(self):
                    QtCore.QObject.__init__(self)
                    self.lastEvent = None
    
    
                ##
                #  Detect a QMainWindow drop event by looking for a NonClientAreaMouseMove (173)
                #  event that immediately follows a Move event
                #
                #  @param    obj    the object that generated the event
                #  @param    event  the current event
                def eventFilter(self, obj, event):
    
                    # If a NonClientAreaMouseMove (173) event immediately follows a Move event...
                    if self.lastEvent == QtCore.QEvent.Move and event.type() == 173:
    
                        # Determine the position of the mouse cursor and emit it with the
                        # onDropSignal
                        mouseCursor = QtGui.QCursor()
                        dropPos = mouseCursor.pos()
                        self.onDropSignal.emit(dropPos)
                        self.lastEvent = event.type()
                        return True
    
                    else:
                        self.lastEvent = event.type()
                        return False
    
    
        ##
        #  The TabBar class re-implements some of the functionality of the QTabBar widget
        class TabBar(QtWidgets.QTabBar):
            onDetachTabSignal = pyqtSignal(int, QtCore.QPoint)
            onMoveTabSignal = pyqtSignal(int, int)
            detachedTabDropSignal = pyqtSignal(str, int, QtCore.QPoint)
    
            def __init__(self, parent=None):
                QtWidgets.QTabBar.__init__(self, parent)
    
                self.setAcceptDrops(True)
                self.setElideMode(QtCore.Qt.ElideRight)
                self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab)
    
                self.dragStartPos = QtCore.QPoint()
                self.dragDropedPos = QtCore.QPoint()
                self.mouseCursor = QtGui.QCursor()
                self.dragInitiated = False
    
    
            ##
            #  Send the onDetachTabSignal when a tab is double clicked
            #
            #  @param    event    a mouse double click event
            def mouseDoubleClickEvent(self, event):
                event.accept()
                self.onDetachTabSignal.emit(self.tabAt(event.pos()), self.mouseCursor.pos())
    
    
            ##
            #  Set the starting position for a drag event when the mouse button is pressed
            #
            #  @param    event    a mouse press event
            def mousePressEvent(self, event):
                if event.button() == QtCore.Qt.LeftButton:
                    self.dragStartPos = event.pos()
    
                self.dragDropedPos.setX(0)
                self.dragDropedPos.setY(0)
    
                self.dragInitiated = False
    
                QtWidgets.QTabBar.mousePressEvent(self, event)
    
    
            ##
            #  Determine if the current movement is a drag.  If it is, convert it into a QDrag.  If the
            #  drag ends inside the tab bar, emit an onMoveTabSignal.  If the drag ends outside the tab
            #  bar, emit an onDetachTabSignal.
            #
            #  @param    event    a mouse move event
            def mouseMoveEvent(self, event):
    
                # Determine if the current movement is detected as a drag
                if not self.dragStartPos.isNull() and ((event.pos() - self.dragStartPos).manhattanLength() < QtWidgets.QApplication.startDragDistance()):
                    self.dragInitiated = True
    
                # If the current movement is a drag initiated by the left button
                if (((event.buttons() & QtCore.Qt.LeftButton)) and self.dragInitiated):
    
                    # Stop the move event
                    finishMoveEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), QtCore.Qt.NoButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
                    QtWidgets.QTabBar.mouseMoveEvent(self, finishMoveEvent)
    
                    # Convert the move event into a drag
                    drag = QtGui.QDrag(self)
                    mimeData = QtCore.QMimeData()
                    # mimeData.setData('action', 'application/tab-detach')
                    drag.setMimeData(mimeData)
                    # screen = QScreen(self.parentWidget().currentWidget().winId())
                    # Create the appearance of dragging the tab content
                    pixmap = self.parent().widget(self.tabAt(self.dragStartPos)).grab()
                    targetPixmap = QtGui.QPixmap(pixmap.size())
                    targetPixmap.fill(QtCore.Qt.transparent)
                    painter = QtGui.QPainter(targetPixmap)
                    painter.setOpacity(0.85)
                    painter.drawPixmap(0, 0, pixmap)
                    painter.end()
                    drag.setPixmap(targetPixmap)
    
                    # Initiate the drag
                    dropAction = drag.exec_(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction)
    
    
                    # For Linux:  Here, drag.exec_() will not return MoveAction on Linux.  So it
                    #             must be set manually
                    if self.dragDropedPos.x() != 0 and self.dragDropedPos.y() != 0:
                        dropAction = QtCore.Qt.MoveAction
    
    
                    # If the drag completed outside of the tab bar, detach the tab and move
                    # the content to the current cursor position
                    if dropAction == QtCore.Qt.IgnoreAction:
                        event.accept()
                        self.onDetachTabSignal.emit(self.tabAt(self.dragStartPos), self.mouseCursor.pos())
    
                    # Else if the drag completed inside the tab bar, move the selected tab to the new position
                    elif dropAction == QtCore.Qt.MoveAction:
                        if not self.dragDropedPos.isNull():
                            event.accept()
                            self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos))
                else:
                    QtWidgets.QTabBar.mouseMoveEvent(self, event)
    
    
            ##
            #  Determine if the drag has entered a tab position from another tab position
            #
            #  @param    event    a drag enter event
            def dragEnterEvent(self, event):
                mimeData = event.mimeData()
                formats = mimeData.formats()
    
           #     if formats.contains('action') and mimeData.data('action') == 'application/tab-detach':
           #       event.acceptProposedAction()
    
                QtWidgets.QTabBar.dragMoveEvent(self, event)
    
    
            ##
            #  Get the position of the end of the drag
            #
            #  @param    event    a drop event
            def dropEvent(self, event):
                self.dragDropedPos = event.pos()
                QtWidgets.QTabBar.dropEvent(self, event)
    
    
            ##
            #  Determine if the detached tab drop event occurred on an existing tab,
            #  then send the event to the DetachableTabWidget
            def detachedTabDrop(self, name, dropPos):
    
                tabDropPos = self.mapFromGlobal(dropPos)
    
                index = self.tabAt(tabDropPos)
    
                self.detachedTabDropSignal.emit(name, index, dropPos)
    
    
    
    if __name__ == '__main__':
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
    
        mainWindow = QtWidgets.QMainWindow()
        tabWidget = DetachableTabWidget()
    
        tab1 = QtWidgets.QLabel('Test Widget 1')
        tabWidget.addTab(tab1, 'Tab1')
    
        tab2 = QtWidgets.QLabel('Test Widget 2')
        tabWidget.addTab(tab2, 'Tab2')
    
        tab3 = QtWidgets.QLabel('Test Widget 3')
        tabWidget.addTab(tab3, 'Tab3')
    
        tabWidget.show()
        mainWindow.setCentralWidget(tabWidget)
        mainWindow.show()
    
        try:
            exitStatus = app.exec_()
            # print 'Done...'
            sys.exit(exitStatus)
        except:
            pass
    

    【讨论】:

    • 第433行有语法错误,写成formats = mcd imeData.formats() 但应该是formats = mimeData.formats()
    • 谢谢,这已经在源代码中修复了!
    【解决方案2】:

    我在 Qt 中心论坛的this post 中找到了一个部分工作的 C++ 示例。这是不完整的和错误的。但是,我能够将其用作参考和起点,以使用 PyQt 创建我自己的 DetachableTabWidget。由于我无法在 PyQt 中找到任何其他功能齐全的示例,因此我想在此处发布此内容。也许它会对某人有用。

    我不会称之为完美,所以我绝对愿意接受任何改进建议。

    EDIT1

    之前的迭代有一些严重的缺陷,直到我尝试在实际应用程序中使用它时才发现这些缺陷。我将 QDialog 用于分离的选项卡,这意味着它们不能像典型窗口一样被最小化或最大化。我还让它们由选项卡主机作为父项,这意味着分离的选项卡始终位于选项卡主机的顶部。以下是我使用 QMainWindow 作为分离标签的新版本。

    EDIT2

    如 Qt 文档中所述,QDrag.exec_() 在 Windows 和 Linux 之间的行为不同。这导致我为 EDIT1 发布的迭代失去了在 Linux 中移动(重新排序)选项卡的能力。我对这个迭代做了一个小修复,使它现在可以在 Windows 和 Linux 上运行。我还更新了 cmets 以反映从 QDialog 到 QMainWindow 的交换。

    EDIT3

    我添加了一个 removeTabByName(name) 函数,该函数将按名称删除选项卡,即使它已分离。

    我添加了通过将选项卡拖回选项卡栏区域来重新附加选项卡的功能。如果它被放在另一个选项卡上,它将被插入到该位置。如果将其放在选项卡栏旁边,则将其作为最后一个选项卡附加。如果所有选项卡都已分离,则将分离的选项卡拖放到 DetachableTabWidget 中的任何位置都会重新附加该选项卡。

    from PyQt4 import QtGui, QtCore
    from PyQt4.QtCore import pyqtSignal, pyqtSlot
    
    ##
    # The DetachableTabWidget adds additional functionality to Qt's QTabWidget that allows it
    # to detach and re-attach tabs.
    #
    # Additional Features:
    #   Detach tabs by
    #     dragging the tabs away from the tab bar
    #     double clicking the tab
    #   Re-attach tabs by
    #     dragging the detached tab's window into the tab bar
    #     closing the detached tab's window
    #   Remove tab (attached or detached) by name
    #
    # Modified Features:
    #   Re-ordering (moving) tabs by dragging was re-implemented  
    #   
    class DetachableTabWidget(QtGui.QTabWidget):
        def __init__(self, parent=None):
            QtGui.QTabWidget.__init__(self, parent)
    
            self.tabBar = self.TabBar(self)
            self.tabBar.onDetachTabSignal.connect(self.detachTab)
            self.tabBar.onMoveTabSignal.connect(self.moveTab)
            self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop)
    
            self.setTabBar(self.tabBar)
    
            # Used to keep a reference to detached tabs since their QMainWindow
            # does not have a parent
            self.detachedTabs = {}
    
            # Close all detached tabs if the application is closed explicitly
            QtGui.qApp.aboutToQuit.connect(self.closeDetachedTabs) # @UndefinedVariable
    
    
        ##
        #  The default movable functionality of QTabWidget must remain disabled
        #  so as not to conflict with the added features
        def setMovable(self, movable):
            pass
    
        ##
        #  Move a tab from one position (index) to another
        #
        #  @param    fromIndex    the original index location of the tab
        #  @param    toIndex      the new index location of the tab
        @pyqtSlot(int, int)
        def moveTab(self, fromIndex, toIndex):
            widget = self.widget(fromIndex)
            icon = self.tabIcon(fromIndex)
            text = self.tabText(fromIndex)
    
            self.removeTab(fromIndex)
            self.insertTab(toIndex, widget, icon, text)
            self.setCurrentIndex(toIndex)
    
    
        ##
        #  Detach the tab by removing it's contents and placing them in
        #  a DetachedTab window
        #
        #  @param    index    the index location of the tab to be detached
        #  @param    point    the screen position for creating the new DetachedTab window
        @pyqtSlot(int, QtCore.QPoint)
        def detachTab(self, index, point):
    
            # Get the tab content
            name = self.tabText(index)
            icon = self.tabIcon(index)        
            if icon.isNull():
                icon = self.window().windowIcon()              
            contentWidget = self.widget(index)
    
            try:
                contentWidgetRect = contentWidget.frameGeometry()
            except AttributeError:
                return
    
            # Create a new detached tab window
            detachedTab = self.DetachedTab(name, contentWidget)
            detachedTab.setWindowModality(QtCore.Qt.NonModal)
            detachedTab.setWindowIcon(icon)
            detachedTab.setGeometry(contentWidgetRect)
            detachedTab.onCloseSignal.connect(self.attachTab)
            detachedTab.onDropSignal.connect(self.tabBar.detachedTabDrop)
            detachedTab.move(point)
            detachedTab.show()
    
    
            # Create a reference to maintain access to the detached tab
            self.detachedTabs[name] = detachedTab
    
    
        ##
        #  Re-attach the tab by removing the content from the DetachedTab window,
        #  closing it, and placing the content back into the DetachableTabWidget
        #
        #  @param    contentWidget    the content widget from the DetachedTab window
        #  @param    name             the name of the detached tab
        #  @param    icon             the window icon for the detached tab
        #  @param    insertAt         insert the re-attached tab at the given index
        def attachTab(self, contentWidget, name, icon, insertAt=None):
    
            # Make the content widget a child of this widget
            contentWidget.setParent(self)
    
    
            # Remove the reference
            del self.detachedTabs[name]
    
    
            # Create an image from the given icon (for comparison)
            if not icon.isNull():
                try:
                    tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
                    tabIconImage = tabIconPixmap.toImage()
                except IndexError:
                    tabIconImage = None
            else:
                tabIconImage = None
    
    
            # Create an image of the main window icon (for comparison)
            if not icon.isNull():
                try:
                    windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
                    windowIconImage = windowIconPixmap.toImage()
                except IndexError:
                    windowIconImage = None
            else:
                windowIconImage = None
    
    
            # Determine if the given image and the main window icon are the same.
            # If they are, then do not add the icon to the tab
            if tabIconImage == windowIconImage:
                if insertAt == None:
                    index = self.addTab(contentWidget, name)
                else:
                    index = self.insertTab(insertAt, contentWidget, name)
            else:
                if insertAt == None:
                    index = self.addTab(contentWidget, icon, name)
                else:
                    index = self.insertTab(insertAt, contentWidget, icon, name)
    
    
            # Make this tab the current tab
            if index > -1:
                self.setCurrentIndex(index)
    
    
        ##
        #  Remove the tab with the given name, even if it is detached
        #
        #  @param    name    the name of the tab to be removed
        def removeTabByName(self, name):
    
            # Remove the tab if it is attached
            attached = False
            for index in xrange(self.count()):
                if str(name) == str(self.tabText(index)):
                    self.removeTab(index)
                    attached = True
                    break
    
    
            # If the tab is not attached, close it's window and
            # remove the reference to it
            if not attached:
                for key in self.detachedTabs:
                    if str(name) == str(key):
                        self.detachedTabs[key].onCloseSignal.disconnect()
                        self.detachedTabs[key].close()
                        del self.detachedTabs[key]
                        break
    
    
        ##
        #  Handle dropping of a detached tab inside the DetachableTabWidget
        #
        #  @param    name     the name of the detached tab
        #  @param    index    the index of an existing tab (if the tab bar
        #                     determined that the drop occurred on an
        #                     existing tab)
        #  @param    dropPos  the mouse cursor position when the drop occurred
        @QtCore.pyqtSlot(QtCore.QString, int, QtCore.QPoint)
        def detachedTabDrop(self, name, index, dropPos):
    
            # If the drop occurred on an existing tab, insert the detached
            # tab at the existing tab's location
            if index > -1:
    
                # Create references to the detached tab's content and icon
                contentWidget = self.detachedTabs[name].contentWidget
                icon = self.detachedTabs[name].windowIcon()
    
                # Disconnect the detached tab's onCloseSignal so that it
                # does not try to re-attach automatically
                self.detachedTabs[name].onCloseSignal.disconnect()
    
                # Close the detached
                self.detachedTabs[name].close()
    
                # Re-attach the tab at the given index
                self.attachTab(contentWidget, name, icon, index)
    
    
            # If the drop did not occur on an existing tab, determine if the drop
            # occurred in the tab bar area (the area to the side of the QTabBar)
            else:
    
                # Find the drop position relative to the DetachableTabWidget
                tabDropPos = self.mapFromGlobal(dropPos)
    
                # If the drop position is inside the DetachableTabWidget...
                if self.rect().contains(tabDropPos):                
    
                    # If the drop position is inside the tab bar area (the
                    # area to the side of the QTabBar) or there are not tabs
                    # currently attached...
                    if tabDropPos.y() < self.tabBar.height() or self.count() == 0:
    
                        # Close the detached tab and allow it to re-attach
                        # automatically
                        self.detachedTabs[name].close()
    
    
        ##
        #  Close all tabs that are currently detached.
        def closeDetachedTabs(self):
            listOfDetachedTabs = []
    
            for key in self.detachedTabs:
                listOfDetachedTabs.append(self.detachedTabs[key])
    
            for detachedTab in listOfDetachedTabs:
                detachedTab.close()
    
    
        ##
        #  When a tab is detached, the contents are placed into this QMainWindow.  The tab
        #  can be re-attached by closing the dialog or by dragging the window into the tab bar
        class DetachedTab(QtGui.QMainWindow):
            onCloseSignal = pyqtSignal(QtGui.QWidget, QtCore.QString, QtGui.QIcon)
            onDropSignal = pyqtSignal(QtCore.QString, QtCore.QPoint)
    
            def __init__(self, name, contentWidget):
                QtGui.QMainWindow.__init__(self, None)
    
                self.setObjectName(name)
                self.setWindowTitle(name)
    
                self.contentWidget = contentWidget
                self.setCentralWidget(self.contentWidget)
                self.contentWidget.show()
    
                self.windowDropFilter = self.WindowDropFilter()
                self.installEventFilter(self.windowDropFilter)
                self.windowDropFilter.onDropSignal.connect(self.windowDropSlot)
    
    
            ##
            #  Handle a window drop event
            #
            #  @param    dropPos    the mouse cursor position of the drop
            @QtCore.pyqtSlot(QtCore.QPoint)
            def windowDropSlot(self, dropPos):
                self.onDropSignal.emit(self.objectName(), dropPos)
    
    
            ##
            #  If the window is closed, emit the onCloseSignal and give the
            #  content widget back to the DetachableTabWidget
            #
            #  @param    event    a close event
            def closeEvent(self, event):
                self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon())
    
    
            ##
            #  An event filter class to detect a QMainWindow drop event
            class WindowDropFilter(QtCore.QObject):
                onDropSignal = pyqtSignal(QtCore.QPoint)
    
                def __init__(self):
                    QtCore.QObject.__init__(self)
                    self.lastEvent = None
    
    
                ##
                #  Detect a QMainWindow drop event by looking for a NonClientAreaMouseMove (173)
                #  event that immediately follows a Move event
                #
                #  @param    obj    the object that generated the event
                #  @param    event  the current event
                def eventFilter(self, obj, event):
    
                    # If a NonClientAreaMouseMove (173) event immediately follows a Move event...
                    if self.lastEvent == QtCore.QEvent.Move and event.type() == 173:
    
                        # Determine the position of the mouse cursor and emit it with the
                        # onDropSignal
                        mouseCursor = QtGui.QCursor()
                        dropPos = mouseCursor.pos()                    
                        self.onDropSignal.emit(dropPos)                    
                        self.lastEvent = event.type()                    
                        return True
    
                    else:
                        self.lastEvent = event.type()
                        return False
    
    
        ##
        #  The TabBar class re-implements some of the functionality of the QTabBar widget
        class TabBar(QtGui.QTabBar):
            onDetachTabSignal = pyqtSignal(int, QtCore.QPoint)
            onMoveTabSignal = pyqtSignal(int, int)
            detachedTabDropSignal = pyqtSignal(QtCore.QString, int, QtCore.QPoint)
    
            def __init__(self, parent=None):
                QtGui.QTabBar.__init__(self, parent)
    
                self.setAcceptDrops(True)
                self.setElideMode(QtCore.Qt.ElideRight)
                self.setSelectionBehaviorOnRemove(QtGui.QTabBar.SelectLeftTab)
    
                self.dragStartPos = QtCore.QPoint()
                self.dragDropedPos = QtCore.QPoint()
                self.mouseCursor = QtGui.QCursor()
                self.dragInitiated = False
    
    
            ##
            #  Send the onDetachTabSignal when a tab is double clicked
            #
            #  @param    event    a mouse double click event
            def mouseDoubleClickEvent(self, event):
                event.accept()
                self.onDetachTabSignal.emit(self.tabAt(event.pos()), self.mouseCursor.pos())
    
    
            ##
            #  Set the starting position for a drag event when the mouse button is pressed
            #
            #  @param    event    a mouse press event
            def mousePressEvent(self, event):
                if event.button() == QtCore.Qt.LeftButton:
                    self.dragStartPos = event.pos()
    
                self.dragDropedPos.setX(0)
                self.dragDropedPos.setY(0)
    
                self.dragInitiated = False
    
                QtGui.QTabBar.mousePressEvent(self, event)
    
    
            ##
            #  Determine if the current movement is a drag.  If it is, convert it into a QDrag.  If the
            #  drag ends inside the tab bar, emit an onMoveTabSignal.  If the drag ends outside the tab
            #  bar, emit an onDetachTabSignal.
            #
            #  @param    event    a mouse move event
            def mouseMoveEvent(self, event):
    
                # Determine if the current movement is detected as a drag
                if not self.dragStartPos.isNull() and ((event.pos() - self.dragStartPos).manhattanLength() < QtGui.QApplication.startDragDistance()):
                    self.dragInitiated = True
    
                # If the current movement is a drag initiated by the left button
                if (((event.buttons() & QtCore.Qt.LeftButton)) and self.dragInitiated):
    
                    # Stop the move event
                    finishMoveEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), QtCore.Qt.NoButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
                    QtGui.QTabBar.mouseMoveEvent(self, finishMoveEvent)
    
                    # Convert the move event into a drag
                    drag = QtGui.QDrag(self)
                    mimeData = QtCore.QMimeData()
                    mimeData.setData('action', 'application/tab-detach')
                    drag.setMimeData(mimeData)
    
                    # Create the appearance of dragging the tab content
                    pixmap = QtGui.QPixmap.grabWindow(self.parentWidget().currentWidget().winId())
                    targetPixmap = QtGui.QPixmap(pixmap.size())
                    targetPixmap.fill(QtCore.Qt.transparent)
                    painter = QtGui.QPainter(targetPixmap)
                    painter.setOpacity(0.85)
                    painter.drawPixmap(0, 0, pixmap)
                    painter.end()
                    drag.setPixmap(targetPixmap)
    
                    # Initiate the drag
                    dropAction = drag.exec_(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction)
    
    
                    # For Linux:  Here, drag.exec_() will not return MoveAction on Linux.  So it
                    #             must be set manually
                    if self.dragDropedPos.x() != 0 and self.dragDropedPos.y() != 0:
                        dropAction = QtCore.Qt.MoveAction
    
    
                    # If the drag completed outside of the tab bar, detach the tab and move
                    # the content to the current cursor position
                    if dropAction == QtCore.Qt.IgnoreAction:
                        event.accept()
                        self.onDetachTabSignal.emit(self.tabAt(self.dragStartPos), self.mouseCursor.pos())
    
                    # Else if the drag completed inside the tab bar, move the selected tab to the new position
                    elif dropAction == QtCore.Qt.MoveAction:
                        if not self.dragDropedPos.isNull():
                            event.accept()
                            self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos))
                else:
                    QtGui.QTabBar.mouseMoveEvent(self, event)
    
    
            ##
            #  Determine if the drag has entered a tab position from another tab position
            #
            #  @param    event    a drag enter event
            def dragEnterEvent(self, event):
                mimeData = event.mimeData()
                formats = mimeData.formats()
    
                if formats.contains('action') and mimeData.data('action') == 'application/tab-detach':
                    event.acceptProposedAction()
    
                QtGui.QTabBar.dragMoveEvent(self, event)
    
    
            ##
            #  Get the position of the end of the drag
            #
            #  @param    event    a drop event
            def dropEvent(self, event):
                self.dragDropedPos = event.pos()
                QtGui.QTabBar.dropEvent(self, event)
    
    
            ##
            #  Determine if the detached tab drop event occurred on an existing tab,
            #  then send the event to the DetachableTabWidget
            def detachedTabDrop(self, name, dropPos):
    
                tabDropPos = self.mapFromGlobal(dropPos)
    
                index = self.tabAt(tabDropPos)
    
                self.detachedTabDropSignal.emit(name, index, dropPos)
    
    
    
    if __name__ == '__main__':
        import sys
    
        app = QtGui.QApplication(sys.argv)
    
        mainWindow = QtGui.QMainWindow()
        tabWidget = DetachableTabWidget()
    
        tab1 = QtGui.QLabel('Test Widget 1')    
        tabWidget.addTab(tab1, 'Tab1')
    
        tab2 = QtGui.QLabel('Test Widget 2')
        tabWidget.addTab(tab2, 'Tab2')
    
        tab3 = QtGui.QLabel('Test Widget 3')
        tabWidget.addTab(tab3, 'Tab3')
    
        tabWidget.show()
        mainWindow.setCentralWidget(tabWidget)
        mainWindow.show()
    
        try:
            exitStatus = app.exec_()
            print 'Done...'
            sys.exit(exitStatus)
        except:
            pass
    

    不足之处

    与网络浏览器不同,关闭窗口(分离的选项卡)将始终将其重新附加到选项卡栏。将来我想添加一个选项,以便能够关闭窗口而不是重新附加。

    我仍然需要添加一个方法来从小部件外部获取对分离选项卡的引用。

    错误

    我们将不胜感激。

    1. 在极少数情况下,通过拖动删除选项卡时,拖动事件不会被检测为拖动。这很少见,我没有花太多时间。
    2. 有时当一个标签被分离时,持有分离的标签的QMainWindow不会产生NonClientAreaMouseMove事件。让它再次开始生成此事件的唯一方法是让 QMainWindow 松开并重新获得焦点。我不确定这是否是我的错误。

    【讨论】:

      【解决方案3】:

      我在发布的 PyQt5 答案中遇到了一些问题,所以我自己重构了 Blackwood 的代码,并在不删除任何部分的情况下让它工作:

      # https://stackoverflow.com/a/50693795/3620725
      
      from PyQt5 import QtGui, QtCore, QtWidgets
      from PyQt5.QtCore import pyqtSignal, pyqtSlot
      
      
      class DetachableTabWidget(QtWidgets.QTabWidget):
          def __init__(self, parent=None):
              QtWidgets.QTabWidget.__init__(self, parent)
      
              self.tabBar = self.TabBar(self)
              self.tabBar.onDetachTabSignal.connect(self.detachTab)
              self.tabBar.onMoveTabSignal.connect(self.moveTab)
              self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop)
      
              self.setTabBar(self.tabBar)
      
              # Used to keep a reference to detached tabs since their QMainWindow
              # does not have a parent
              self.detachedTabs = {}
      
              # Close all detached tabs if the application is closed explicitly
              QtWidgets.qApp.aboutToQuit.connect(self.closeDetachedTabs)  # @UndefinedVariable
      
          ##
          #  The default movable functionality of QTabWidget must remain disabled
          #  so as not to conflict with the added features
          def setMovable(self, movable):
              pass
      
          ##
          #  Move a tab from one position (index) to another
          #
          #  @param    fromIndex    the original index location of the tab
          #  @param    toIndex      the new index location of the tab
          @pyqtSlot(int, int)
          def moveTab(self, fromIndex, toIndex):
              widget = self.widget(fromIndex)
              icon = self.tabIcon(fromIndex)
              text = self.tabText(fromIndex)
      
              self.removeTab(fromIndex)
              self.insertTab(toIndex, widget, icon, text)
              self.setCurrentIndex(toIndex)
      
          ##
          #  Detach the tab by removing it's contents and placing them in
          #  a DetachedTab window
          #
          #  @param    index    the index location of the tab to be detached
          #  @param    point    the screen position for creating the new DetachedTab window
          @pyqtSlot(int, QtCore.QPoint)
          def detachTab(self, index, point):
      
              # Get the tab content
              name = self.tabText(index)
              icon = self.tabIcon(index)
              if icon.isNull():
                  icon = self.window().windowIcon()
              contentWidget = self.widget(index)
      
              try:
                  contentWidgetRect = contentWidget.frameGeometry()
              except AttributeError:
                  return
      
              # Create a new detached tab window
              detachedTab = self.DetachedTab(name, contentWidget)
              detachedTab.setWindowModality(QtCore.Qt.NonModal)
              detachedTab.setWindowIcon(icon)
              detachedTab.setGeometry(contentWidgetRect)
              detachedTab.onCloseSignal.connect(self.attachTab)
              detachedTab.onDropSignal.connect(self.tabBar.detachedTabDrop)
              detachedTab.move(point)
              detachedTab.show()
      
              # Create a reference to maintain access to the detached tab
              self.detachedTabs[name] = detachedTab
      
          ##
          #  Re-attach the tab by removing the content from the DetachedTab window,
          #  closing it, and placing the content back into the DetachableTabWidget
          #
          #  @param    contentWidget    the content widget from the DetachedTab window
          #  @param    name             the name of the detached tab
          #  @param    icon             the window icon for the detached tab
          #  @param    insertAt         insert the re-attached tab at the given index
          def attachTab(self, contentWidget, name, icon, insertAt=None):
      
              # Make the content widget a child of this widget
              contentWidget.setParent(self)
      
              # Remove the reference
              del self.detachedTabs[name]
      
              # Create an image from the given icon (for comparison)
              if not icon.isNull():
                  try:
                      tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
                      tabIconImage = tabIconPixmap.toImage()
                  except IndexError:
                      tabIconImage = None
              else:
                  tabIconImage = None
      
              # Create an image of the main window icon (for comparison)
              if not icon.isNull():
                  try:
                      windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
                      windowIconImage = windowIconPixmap.toImage()
                  except IndexError:
                      windowIconImage = None
              else:
                  windowIconImage = None
      
              # Determine if the given image and the main window icon are the same.
              # If they are, then do not add the icon to the tab
              if tabIconImage == windowIconImage:
                  if insertAt == None:
                      index = self.addTab(contentWidget, name)
                  else:
                      index = self.insertTab(insertAt, contentWidget, name)
              else:
                  if insertAt == None:
                      index = self.addTab(contentWidget, icon, name)
                  else:
                      index = self.insertTab(insertAt, contentWidget, icon, name)
      
              # Make this tab the current tab
              if index > -1:
                  self.setCurrentIndex(index)
      
          ##
          #  Remove the tab with the given name, even if it is detached
          #
          #  @param    name    the name of the tab to be removed
          def removeTabByName(self, name):
      
              # Remove the tab if it is attached
              attached = False
              for index in range(self.count()):
                  if str(name) == str(self.tabText(index)):
                      self.removeTab(index)
                      attached = True
                      break
      
              # If the tab is not attached, close it's window and
              # remove the reference to it
              if not attached:
                  for key in self.detachedTabs:
                      if str(name) == str(key):
                          self.detachedTabs[key].onCloseSignal.disconnect()
                          self.detachedTabs[key].close()
                          del self.detachedTabs[key]
                          break
      
          ##
          #  Handle dropping of a detached tab inside the DetachableTabWidget
          #
          #  @param    name     the name of the detached tab
          #  @param    index    the index of an existing tab (if the tab bar
          #                     determined that the drop occurred on an
          #                     existing tab)
          #  @param    dropPos  the mouse cursor position when the drop occurred
          @QtCore.pyqtSlot(str, int, QtCore.QPoint)
          def detachedTabDrop(self, name, index, dropPos):
      
              # If the drop occurred on an existing tab, insert the detached
              # tab at the existing tab's location
              if index > -1:
      
                  # Create references to the detached tab's content and icon
                  contentWidget = self.detachedTabs[name].contentWidget
                  icon = self.detachedTabs[name].windowIcon()
      
                  # Disconnect the detached tab's onCloseSignal so that it
                  # does not try to re-attach automatically
                  self.detachedTabs[name].onCloseSignal.disconnect()
      
                  # Close the detached
                  self.detachedTabs[name].close()
      
                  # Re-attach the tab at the given index
                  self.attachTab(contentWidget, name, icon, index)
      
      
              # If the drop did not occur on an existing tab, determine if the drop
              # occurred in the tab bar area (the area to the side of the QTabBar)
              else:
      
                  # Find the drop position relative to the DetachableTabWidget
                  tabDropPos = self.mapFromGlobal(dropPos)
      
                  # If the drop position is inside the DetachableTabWidget...
                  if tabDropPos in self.rect():
      
                      # If the drop position is inside the tab bar area (the
                      # area to the side of the QTabBar) or there are not tabs
                      # currently attached...
                      if tabDropPos.y() < self.tabBar.height() or self.count() == 0:
                          # Close the detached tab and allow it to re-attach
                          # automatically
                          self.detachedTabs[name].close()
      
          ##
          #  Close all tabs that are currently detached.
          def closeDetachedTabs(self):
              listOfDetachedTabs = []
      
              for key in self.detachedTabs:
                  listOfDetachedTabs.append(self.detachedTabs[key])
      
              for detachedTab in listOfDetachedTabs:
                  detachedTab.close()
      
          ##
          #  When a tab is detached, the contents are placed into this QMainWindow.  The tab
          #  can be re-attached by closing the dialog or by dragging the window into the tab bar
          class DetachedTab(QtWidgets.QMainWindow):
              onCloseSignal = pyqtSignal(QtWidgets.QWidget, str, QtGui.QIcon)
              onDropSignal = pyqtSignal(str, QtCore.QPoint)
      
              def __init__(self, name, contentWidget):
                  QtWidgets.QMainWindow.__init__(self, None)
      
                  self.setObjectName(name)
                  self.setWindowTitle(name)
      
                  self.contentWidget = contentWidget
                  self.setCentralWidget(self.contentWidget)
                  self.contentWidget.show()
      
                  self.windowDropFilter = self.WindowDropFilter()
                  self.installEventFilter(self.windowDropFilter)
                  self.windowDropFilter.onDropSignal.connect(self.windowDropSlot)
      
              ##
              #  Handle a window drop event
              #
              #  @param    dropPos    the mouse cursor position of the drop
              @QtCore.pyqtSlot(QtCore.QPoint)
              def windowDropSlot(self, dropPos):
                  self.onDropSignal.emit(self.objectName(), dropPos)
      
              ##
              #  If the window is closed, emit the onCloseSignal and give the
              #  content widget back to the DetachableTabWidget
              #
              #  @param    event    a close event
              def closeEvent(self, event):
                  self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon())
      
              ##
              #  An event filter class to detect a QMainWindow drop event
              class WindowDropFilter(QtCore.QObject):
                  onDropSignal = pyqtSignal(QtCore.QPoint)
      
                  def __init__(self):
                      QtCore.QObject.__init__(self)
                      self.lastEvent = None
      
                  ##
                  #  Detect a QMainWindow drop event by looking for a NonClientAreaMouseMove (173)
                  #  event that immediately follows a Move event
                  #
                  #  @param    obj    the object that generated the event
                  #  @param    event  the current event
                  def eventFilter(self, obj, event):
      
                      # If a NonClientAreaMouseMove (173) event immediately follows a Move event...
                      if self.lastEvent == QtCore.QEvent.Move and event.type() == 173:
      
                          # Determine the position of the mouse cursor and emit it with the
                          # onDropSignal
                          mouseCursor = QtGui.QCursor()
                          dropPos = mouseCursor.pos()
                          self.onDropSignal.emit(dropPos)
                          self.lastEvent = event.type()
                          return True
      
                      else:
                          self.lastEvent = event.type()
                          return False
      
          ##
          #  The TabBar class re-implements some of the functionality of the QTabBar widget
          class TabBar(QtWidgets.QTabBar):
              onDetachTabSignal = pyqtSignal(int, QtCore.QPoint)
              onMoveTabSignal = pyqtSignal(int, int)
              detachedTabDropSignal = pyqtSignal(str, int, QtCore.QPoint)
      
              def __init__(self, parent=None):
                  QtWidgets.QTabBar.__init__(self, parent)
      
                  self.setAcceptDrops(True)
                  self.setElideMode(QtCore.Qt.ElideRight)
                  self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab)
      
                  self.dragStartPos = QtCore.QPoint()
                  self.dragDropedPos = QtCore.QPoint()
                  self.mouseCursor = QtGui.QCursor()
                  self.dragInitiated = False
      
              ##
              #  Send the onDetachTabSignal when a tab is double clicked
              #
              #  @param    event    a mouse double click event
              def mouseDoubleClickEvent(self, event):
                  event.accept()
                  self.onDetachTabSignal.emit(self.tabAt(event.pos()), self.mouseCursor.pos())
      
              ##
              #  Set the starting position for a drag event when the mouse button is pressed
              #
              #  @param    event    a mouse press event
              def mousePressEvent(self, event):
                  if event.button() == QtCore.Qt.LeftButton:
                      self.dragStartPos = event.pos()
      
                  self.dragDropedPos.setX(0)
                  self.dragDropedPos.setY(0)
      
                  self.dragInitiated = False
      
                  QtWidgets.QTabBar.mousePressEvent(self, event)
      
              ##
              #  Determine if the current movement is a drag.  If it is, convert it into a QDrag.  If the
              #  drag ends inside the tab bar, emit an onMoveTabSignal.  If the drag ends outside the tab
              #  bar, emit an onDetachTabSignal.
              #
              #  @param    event    a mouse move event
              def mouseMoveEvent(self, event):
      
                  # Determine if the current movement is detected as a drag
                  if not self.dragStartPos.isNull() and (
                          (event.pos() - self.dragStartPos).manhattanLength() < QtWidgets.QApplication.startDragDistance()):
                      self.dragInitiated = True
      
                  # If the current movement is a drag initiated by the left button
                  if (((event.buttons() & QtCore.Qt.LeftButton)) and self.dragInitiated):
      
                      # Stop the move event
                      finishMoveEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), QtCore.Qt.NoButton,
                                                          QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
                      QtWidgets.QTabBar.mouseMoveEvent(self, finishMoveEvent)
      
                      # Convert the move event into a drag
                      drag = QtGui.QDrag(self)
                      mimeData = QtCore.QMimeData()
                      mimeData.setData('action', b'application/tab-detach')
                      drag.setMimeData(mimeData)
      
                      # Create the appearance of dragging the tab content
                      pixmap = self.parentWidget().currentWidget().grab()
                      targetPixmap = QtGui.QPixmap(pixmap.size())
                      targetPixmap.fill(QtCore.Qt.transparent)
                      painter = QtGui.QPainter(targetPixmap)
                      painter.setOpacity(0.85)
                      painter.drawPixmap(0, 0, pixmap)
                      painter.end()
                      drag.setPixmap(targetPixmap)
      
                      # Initiate the drag
                      dropAction = drag.exec_(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction)
      
                      # For Linux:  Here, drag.exec_() will not return MoveAction on Linux.  So it
                      #             must be set manually
                      if self.dragDropedPos.x() != 0 and self.dragDropedPos.y() != 0:
                          dropAction = QtCore.Qt.MoveAction
      
                      # If the drag completed outside of the tab bar, detach the tab and move
                      # the content to the current cursor position
                      if dropAction == QtCore.Qt.IgnoreAction:
                          event.accept()
                          self.onDetachTabSignal.emit(self.tabAt(self.dragStartPos), self.mouseCursor.pos())
      
                      # Else if the drag completed inside the tab bar, move the selected tab to the new position
                      elif dropAction == QtCore.Qt.MoveAction:
                          if not self.dragDropedPos.isNull():
                              event.accept()
                              self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos))
                  else:
                      QtWidgets.QTabBar.mouseMoveEvent(self, event)
      
              ##
              #  Determine if the drag has entered a tab position from another tab position
              #
              #  @param    event    a drag enter event
              def dragEnterEvent(self, event):
                  mimeData = event.mimeData()
                  formats = mimeData.formats()
      
                  if 'action' in formats and mimeData.data('action') == 'application/tab-detach':
                      event.acceptProposedAction()
      
                  QtWidgets.QTabBar.dragMoveEvent(self, event)
      
              ##
              #  Get the position of the end of the drag
              #
              #  @param    event    a drop event
              def dropEvent(self, event):
                  self.dragDropedPos = event.pos()
                  QtWidgets.QTabBar.dropEvent(self, event)
      
              ##
              #  Determine if the detached tab drop event occurred on an existing tab,
              #  then send the event to the DetachableTabWidget
              def detachedTabDrop(self, name, dropPos):
      
                  tabDropPos = self.mapFromGlobal(dropPos)
      
                  index = self.tabAt(tabDropPos)
      
                  self.detachedTabDropSignal.emit(name, index, dropPos)
      
      
      if __name__ == '__main__':
          import sys
      
          app = QtWidgets.QApplication(sys.argv)
      
          tabWidget = DetachableTabWidget()
      
          tab1 = QtWidgets.QLabel('Test Widget 1')
          tabWidget.addTab(tab1, 'Tab1')
      
          tab2 = QtWidgets.QLabel('Test Widget 2')
          tabWidget.addTab(tab2, 'Tab2')
      
          tab3 = QtWidgets.QLabel('Test Widget 3')
          tabWidget.addTab(tab3, 'Tab3')
      
          tabWidget.show()
      
          app.exec_()
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-23
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多