Issue
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()
Solution
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
Answered By - Hitesh Patel
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.