问题
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 attachTab
function 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 mouseMoveEvent
function :
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()
回答1:
This is code developed on PyQt4 for detachable tab widget in PyQt. Hope this will help you.
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
# closing the detached tab's window
# double clicking the detached tab's window frame
#
# 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.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, 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)
contentWidgetRect = contentWidget.frameGeometry()
# Create a new detached tab window
detachedTab = self.DetachedTab(contentWidget, self.parentWidget())
detachedTab.setWindowModality(QtCore.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(QtGui.QWidget, QtCore.QString, QtGui.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(QtGui.QDialog):
onCloseSignal = pyqtSignal(QtGui.QWidget, QtCore.QString, QtGui.QIcon)
def __init__(self, contentWidget, parent=None):
QtGui.QDialog.__init__(self, parent)
layout = QtGui.QVBoxLayout(self)
self.contentWidget = contentWidget
layout.addWidget(self.contentWidget)
self.contentWidget.show()
##
# 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 QtGui.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(QtGui.QTabBar):
onDetachTabSignal = pyqtSignal(int, QtCore.QPoint)
onMoveTabSignal = pyqtSignal(int, int)
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)
# 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)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
mainWindow = QtGui.QMainWindow()
tabWidget = DetachableTabWidget(mainWindow)
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
来源:https://stackoverflow.com/questions/48901854/is-it-possible-to-drag-a-qtabwidget-and-open-a-new-window-containing-whats-in-t