[Python] Is it possible to drag a QTabWidget and open a new window containing what's in this...

Discussão em 'Python' iniciado por Stack, Fevereiro 27, 2025.

  1. Stack

    Stack Membro Participativo

    I'm wondering if it is possible by clicking and dragging a Tab to open a new window with what was in that Tab. If it's possible, I would like to also do the reverse: dragging the new window inside the Tabs (where it was in the first place).

    I don't know how should I start. I read in some forums that all must be coded but I don't know if Qt allows some facilities to do that?

    Here a code as a starting point:

    from PyQt5.QtGui import *
    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *
    import sys


    class SurfViewer(QMainWindow):
    def __init__(self, parent=None):
    super(SurfViewer, self).__init__()
    self.parent = parent
    self.centralTabs= QTabWidget()
    self.setCentralWidget(self.centralTabs)
    self.setFixedWidth(200)
    self.setFixedHeight(200)

    #tab 1
    self.tab_1 = QWidget()
    self.centralTabs.addTab(self.tab_1,"Label")
    vbox = QVBoxLayout()
    Label = QLabel('Tab1')
    Label.setFixedWidth(180)
    LineEdit = QLineEdit('Tab1')
    LineEdit.setFixedWidth(180)
    vbox.addWidget(Label)
    vbox.addWidget(LineEdit)
    vbox.setAlignment(Qt.AlignTop)
    self.tab_1.setLayout(vbox)

    #tab 2
    self.tab_2 = QWidget()
    self.centralTabs.addTab(self.tab_2,"Label")
    vbox = QVBoxLayout()
    Label = QLabel('Tab2')
    Label.setFixedWidth(180)
    LineEdit = QLineEdit('Tab2')
    LineEdit.setFixedWidth(180)
    vbox.addWidget(Label)
    vbox.addWidget(LineEdit)
    vbox.setAlignment(Qt.AlignTop)
    self.tab_2.setLayout(vbox)

    if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = SurfViewer(app)
    ex.setWindowTitle('window')
    ex.show()
    sys.exit(app.exec_( ))


    This is far from my Qt level, so I'm asking some help. If I well understood, I need to reimplement the mousePressEvent() and the dragMoveEvent() of the QTabWidget? Their is this topic: In PyQt4, is it possible to detach tabs from a QTabWidget? but it is with PYQT4 and I'm using PYQT5.

    update


    So, according to In PyQt4, is it possible to detach tabs from a QTabWidget? and after conversion toward pyqt5

    from PyQt5.QtGui import *
    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *
    import sys

    class DetachableTabWidget(QTabWidget):
    def __init__(self, parent=None):
    QTabWidget.__init__(self, parent)

    self.tabBar = self.TabBar(self)
    self.tabBar.onDetachTabSignal.connect(self.detachTab)
    self.tabBar.onMoveTabSignal.connect(self.moveTab)

    self.setTabBar(self.tabBar)


    ##
    # 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 dialog
    #
    # @param index the index location of the tab to be detached
    # @param point the screen position for creating the new DetachedTab dialog
    @pyqtSlot(int, 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)
    contentWidgetRect = contentWidget.frameGeometry()

    # Create a new detached tab window
    detachedTab = self.DetachedTab(contentWidget, self.parentWidget())
    detachedTab.setWindowModality(Qt.NonModal)
    detachedTab.setWindowTitle(name)
    detachedTab.setWindowIcon(icon)
    detachedTab.setObjectName(name)
    detachedTab.setGeometry(contentWidgetRect)
    detachedTab.onCloseSignal.connect(self.attachTab)
    detachedTab.move(point)
    detachedTab.show()


    ##
    # Re-attach the tab by removing the content from the DetachedTab dialog,
    # closing it, and placing the content back into the DetachableTabWidget
    #
    # @param contentWidget the content widget from the DetachedTab dialog
    # @param name the name of the detached tab
    # @param icon the window icon for the detached tab
    @pyqtSlot(QWidget, type(''), QIcon)
    def attachTab(self, contentWidget, name, icon):

    # Make the content widget a child of this widget
    contentWidget.setParent(self)


    # Create an image from the given icon
    if not icon.isNull():
    tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
    tabIconImage = tabIconPixmap.toImage()
    else:
    tabIconImage = None


    # Create an image of the main window icon
    if not icon.isNull():
    windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
    windowIconImage = windowIconPixmap.toImage()
    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:
    index = self.addTab(contentWidget, name)
    else:
    index = self.addTab(contentWidget, icon, name)


    # Make this tab the current tab
    if index > -1:
    self.setCurrentIndex(index)


    ##
    # When a tab is detached, the contents are placed into this QDialog. The tab
    # can be re-attached by closing the dialog or by double clicking on its
    # window frame.
    class DetachedTab(QDialog):
    onCloseSignal = pyqtSignal(QWidget,type(''), QIcon)

    def __init__(self, contentWidget, parent=None):
    QDialog.__init__(self, parent)

    layout = QVBoxLayout(self)
    self.contentWidget = contentWidget
    layout.addWidget(self.contentWidget)
    self.contentWidget.show()
    self.setWindowFlags(Qt.Window)


    ##
    # Capture a double click event on the dialog's window frame
    #
    # @param event an event
    #
    # @return true if the event was recognized
    def event(self, event):

    # If the event type is QEvent.NonClientAreaMouseButtonDblClick then
    # close the dialog
    if event.type() == 176:
    event.accept()
    self.close()

    return QDialog.event(self, event)


    ##
    # If the dialog 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())


    ##
    # The TabBar class re-implements some of the functionality of the QTabBar widget
    class TabBar(QTabBar):
    onDetachTabSignal = pyqtSignal(int, QPoint)
    onMoveTabSignal = pyqtSignal(int, int)

    def __init__(self, parent=None):
    QTabBar.__init__(self, parent)

    self.setAcceptDrops(True)
    self.setElideMode(Qt.ElideRight)
    self.setSelectionBehaviorOnRemove(QTabBar.SelectLeftTab)

    self.dragStartPos = QPoint()
    self.dragDropedPos = QPoint()
    self.mouseCursor = 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() == Qt.LeftButton:
    self.dragStartPos = event.pos()

    self.dragDropedPos.setX(0)
    self.dragDropedPos.setY(0)

    self.dragInitiated = False

    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() < QApplication.startDragDistance()):
    self.dragInitiated = True

    # If the current movement is a drag initiated by the left button
    if (((event.buttons() & Qt.LeftButton)) and self.dragInitiated):

    # Stop the move event
    finishMoveEvent = QMouseEvent(QEvent.MouseMove, event.pos(), Qt.NoButton, Qt.NoButton, Qt.NoModifier)
    QTabBar.mouseMoveEvent(self, finishMoveEvent)

    # Convert the move event into a drag
    drag = QDrag(self)
    mimeData = QMimeData()
    mimeData.setData('action', b'application/tab-detach')
    drag.setMimeData(mimeData)

    #Create the appearance of dragging the tab content
    pixmap = self.parentWidget().grab()
    targetPixmap = QPixmap(pixmap.size())
    targetPixmap.fill(Qt.transparent)
    painter = QPainter(targetPixmap)
    painter.setOpacity(0.85)
    painter.drawPixmap(0, 0, pixmap)
    painter.end()
    drag.setPixmap(targetPixmap)

    # Initiate the drag
    dropAction = drag.exec_(Qt.MoveAction | Qt.CopyAction)

    # If the drag completed outside of the tab bar, detach the tab and move
    # the content to the current cursor position
    if dropAction == 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 == Qt.MoveAction:
    if not self.dragDropedPos.isNull():
    event.accept()
    self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos))
    else:
    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()

    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()
    QTabBar.dropEvent(self, event)

    class SurfViewer(QMainWindow):
    def __init__(self, parent=None):
    super(SurfViewer, self).__init__()
    self.parent = parent
    self.centralTabs= DetachableTabWidget()
    self.setCentralWidget(self.centralTabs)
    self.setFixedWidth(200)
    self.setFixedHeight(200)

    #tab 1
    self.tab_1 = QWidget()
    self.centralTabs.addTab(self.tab_1,"Label")
    vbox = QVBoxLayout()
    Label = QLabel('Tab1')
    Label.setFixedWidth(180)
    LineEdit = QLineEdit('Tab1')
    LineEdit.setFixedWidth(180)
    vbox.addWidget(Label)
    vbox.addWidget(LineEdit)
    vbox.setAlignment(Qt.AlignTop)
    self.tab_1.setLayout(vbox)

    #tab 2
    self.tab_2 = QWidget()
    self.centralTabs.addTab(self.tab_2,"Label")
    vbox = QVBoxLayout()
    Label = QLabel('Tab2')
    Label.setFixedWidth(180)
    LineEdit = QLineEdit('Tab2')
    LineEdit.setFixedWidth(180)
    vbox.addWidget(Label)
    vbox.addWidget(LineEdit)
    vbox.setAlignment(Qt.AlignTop)
    self.tab_2.setLayout(vbox)

    if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = SurfViewer(app)
    ex.setWindowTitle('window')
    ex.show()
    sys.exit(app.exec_( ))


    It stil have one issue with that code. When several Tabs are detatched and when I close them in the wrong order, the Tabs are going in the wrong order in the main window. I would like to have them in the same order as originally

    Update 2


    So I rewrite the attachTabfunction in order to put back the Tab where they were before detachment:

    def attachTab(self, contentWidget, name, icon):

    # Make the content widget a child of this widget
    contentWidget.setParent(self)


    # Create an image from the given icon
    if not icon.isNull():
    tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
    tabIconImage = tabIconPixmap.toImage()
    else:
    tabIconImage = None


    # Create an image of the main window icon
    if not icon.isNull():
    windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
    windowIconImage = windowIconPixmap.toImage()
    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 name == 'Model Selection':
    index = 0
    elif name == "Model' Parameters":
    index = 1
    elif name == 'Stim settings':
    index = 2
    elif name == 'Parameter evolution settings':
    index = 3
    elif name == 'LambdaE':
    index = 4
    elif name == 'Simulation settings':
    index = 5
    elif name == 'LFP + PPS + Pulse Results':
    index = 6
    if tabIconImage == windowIconImage:
    index = self.insertTab(index,contentWidget, name)
    # index = self.addTab(contentWidget, name)
    else:
    # index = self.addTab(contentWidget, icon, name)
    index = self.insertTab(index,contentWidget, icon, name)



    # Make this tab the current tab
    if index > -1:
    self.setCurrentIndex(index)


    So every Tab is insert according to the starting position, but everything is done by hand. Maybe it exists an automatic way to do that.

    I also increase the drag minimal distance because it seems too short too me ni the mouseMoveEventfunction :

    if not self.dragStartPos.isNull() and ((event.pos() - self.dragStartPos).manhattanLength() > QApplication.startDragDistance()*2):


    Ialso modify the < to > to initiate the drag only if the distance is greater than startDragDistance()

    Continue reading...

Compartilhe esta Página