from qtpy.QtGui import * from qtpy.QtCore import * from qtpy.QtWidgets import * class DetachTabWidget(QTabWidget): def __init__(self, parent=None): super(DetachTabWidget, self).__init__(parent) self.tabBar = self.TabBar(self) self.tabBar.onDetachTabSignal.connect(self.detachTab) self.tabBar.onMoveTabSignal.connect(self.moveTab) self.setTabBar(self.tabBar) self.btn = QPushButton(self) self.btn.setEnabled(True) #self.btn.setMinimumSize(64, 40) #self.btn.setIconSize(QSize(38, 38)) self.btn.setIcon(QIcon(':/qt-project.org/styles/commonstyle/images/up-32.png')) self.btn.clicked.connect(self.invokeDetachTab) self.setCornerWidget(self.btn) self._lastgeometry = None @Slot(bool) def invokeDetachTab(self, **args): x = self.btn.geometry().x() y = self.btn.geometry().y() self.detachTab(self.currentIndex(), QPoint(x,y)) ## # 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 @Slot(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 @Slot(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 + ' - Close to reattach') detachedTab.setWindowIcon(icon) detachedTab.setObjectName(name) if self._lastgeometry is None: detachedTab.setGeometry(contentWidgetRect) detachedTab.move(point) else: detachedTab.setGeometry(self._lastgeometry) detachedTab.onCloseSignal.connect(self.attachTab) detachedTab.show() ## # Re-attach the tab by removing the content from the DetachedTab dialog, # closing it, and placing the content back into the DetachTabWidget # # @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 @Slot(QWidget, type(''), QIcon, QRect) def attachTab(self, contentWidget, name, icon, geometry): self._lastgeometry = geometry # 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 = Signal(QWidget,type(''), QIcon, QRect) 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 DetachTabWidget # # @param event a close event def closeEvent(self, event): self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon(),self.geometry()) ## # The TabBar class re-implements some of the functionality of the QTabBar widget class TabBar(QTabBar): onDetachTabSignal = Signal(int, QPoint) onMoveTabSignal = Signal(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= DetachTabWidget() self.setCentralWidget(self.centralTabs) # self.setFixedWidth(200) #self.setFixedHeight(200) #tab 1 self.tab_1 = QWidget() self.centralTabs.addTab(self.tab_1,"Label 1") 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 2") 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__': import sys app = QApplication(sys.argv) ex = SurfViewer(app) ex.setWindowTitle('window') ex.show() sys.exit(app.exec_( ))