Draggable PyQt5 tabs in custom TabWidget work in min example, but not in practice
-
wrote on 29 Nov 2019, 06:34 last edited by
Here's the min example code that I put together from other sources found on web.
from PyQt5.QtWidgets import QTabWidget from PyQt5.QtCore import Qt, QPoint, QMimeData from PyQt5.QtGui import QPixmap, QRegion, QDrag, QCursor class TabWidget(QTabWidget): def __init__(self, parent=None, new=None): super().__init__(parent) self.setAcceptDrops(True) self.tabBar().setMouseTracking(True) self.setMovable(True) if new: TabWidget.setup(self) def __setstate__(self, data): self.__init__(new=False) self.setParent(data['parent']) for widget, tabname in data['tabs']: self.addTab(widget, tabname) TabWidget.setup(self) def __getstate__(self): data = { 'parent' : self.parent(), 'tabs' : [], } tab_list = data['tabs'] for k in range(self.count()): tab_name = self.tabText(k) widget = self.widget(k) tab_list.append((widget, tab_name)) return data def setup(self): pass def mouseMoveEvent(self, e): if e.buttons() != Qt.RightButton: return globalPos = self.mapToGlobal(e.pos()) tabBar = self.tabBar() posInTab = tabBar.mapFromGlobal(globalPos) print(e.pos()) index = tabBar.tabAt(e.pos()) tabBar.dragged_content = self.widget(index) tabBar.dragged_tabname = self.tabText(index) tabRect = tabBar.tabRect(index) pixmap = QPixmap(tabRect.size()) tabBar.render(pixmap,QPoint(),QRegion(tabRect)) mimeData = QMimeData() drag = QDrag(tabBar) drag.setMimeData(mimeData) drag.setPixmap(pixmap) cursor = QCursor(Qt.OpenHandCursor) drag.setHotSpot(e.pos() - posInTab) drag.setDragCursor(cursor.pixmap(),Qt.MoveAction) drag.exec_(Qt.MoveAction) def dragEnterEvent(self, e): e.accept() #self.parent().dragged_index = self.indexOf(self.widget(self.dragged_index)) def dragLeaveEvent(self,e): e.accept() def dropEvent(self, e): if e.source().parentWidget() == self: return e.setDropAction(Qt.MoveAction) e.accept() tabBar = e.source() self.addTab(tabBar.dragged_content, tabBar.dragged_tabname) if __name__ == '__main__': from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout import sys class Window(QWidget): def __init__(self): super().__init__() self.dragged_index = None tabWidgetOne = TabWidget(self) tabWidgetTwo = TabWidget(self) tabWidgetOne.addTab(QWidget(), "tab1") tabWidgetTwo.addTab(QWidget(), "tab2") layout = QHBoxLayout() self.moveWidget = None layout.addWidget(tabWidgetOne) layout.addWidget(tabWidgetTwo) self.setLayout(layout) app = QApplication(sys.argv) window = Window() window1 = Window() window.show() window1.show() sys.exit(app.exec_())
It demonstrates that the concept works also between windows started from the same application instance.
However, when I deploy this class to my real world scenario. Namely this GUI:
It doesn't work. I've found that
tabBar.tabAt(event.pos())
is returning -1 meaning it can't find the tab that I'm pressing on and attempting to drag.Any intuitive reason why this should be happening in my app and not in the minimal example? My app does nothing fancy, and in particular nothing that would cause this to fail, just a standard application of custom MainWindow and etc widgets.
-
Here's the min example code that I put together from other sources found on web.
from PyQt5.QtWidgets import QTabWidget from PyQt5.QtCore import Qt, QPoint, QMimeData from PyQt5.QtGui import QPixmap, QRegion, QDrag, QCursor class TabWidget(QTabWidget): def __init__(self, parent=None, new=None): super().__init__(parent) self.setAcceptDrops(True) self.tabBar().setMouseTracking(True) self.setMovable(True) if new: TabWidget.setup(self) def __setstate__(self, data): self.__init__(new=False) self.setParent(data['parent']) for widget, tabname in data['tabs']: self.addTab(widget, tabname) TabWidget.setup(self) def __getstate__(self): data = { 'parent' : self.parent(), 'tabs' : [], } tab_list = data['tabs'] for k in range(self.count()): tab_name = self.tabText(k) widget = self.widget(k) tab_list.append((widget, tab_name)) return data def setup(self): pass def mouseMoveEvent(self, e): if e.buttons() != Qt.RightButton: return globalPos = self.mapToGlobal(e.pos()) tabBar = self.tabBar() posInTab = tabBar.mapFromGlobal(globalPos) print(e.pos()) index = tabBar.tabAt(e.pos()) tabBar.dragged_content = self.widget(index) tabBar.dragged_tabname = self.tabText(index) tabRect = tabBar.tabRect(index) pixmap = QPixmap(tabRect.size()) tabBar.render(pixmap,QPoint(),QRegion(tabRect)) mimeData = QMimeData() drag = QDrag(tabBar) drag.setMimeData(mimeData) drag.setPixmap(pixmap) cursor = QCursor(Qt.OpenHandCursor) drag.setHotSpot(e.pos() - posInTab) drag.setDragCursor(cursor.pixmap(),Qt.MoveAction) drag.exec_(Qt.MoveAction) def dragEnterEvent(self, e): e.accept() #self.parent().dragged_index = self.indexOf(self.widget(self.dragged_index)) def dragLeaveEvent(self,e): e.accept() def dropEvent(self, e): if e.source().parentWidget() == self: return e.setDropAction(Qt.MoveAction) e.accept() tabBar = e.source() self.addTab(tabBar.dragged_content, tabBar.dragged_tabname) if __name__ == '__main__': from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout import sys class Window(QWidget): def __init__(self): super().__init__() self.dragged_index = None tabWidgetOne = TabWidget(self) tabWidgetTwo = TabWidget(self) tabWidgetOne.addTab(QWidget(), "tab1") tabWidgetTwo.addTab(QWidget(), "tab2") layout = QHBoxLayout() self.moveWidget = None layout.addWidget(tabWidgetOne) layout.addWidget(tabWidgetTwo) self.setLayout(layout) app = QApplication(sys.argv) window = Window() window1 = Window() window.show() window1.show() sys.exit(app.exec_())
It demonstrates that the concept works also between windows started from the same application instance.
However, when I deploy this class to my real world scenario. Namely this GUI:
It doesn't work. I've found that
tabBar.tabAt(event.pos())
is returning -1 meaning it can't find the tab that I'm pressing on and attempting to drag.Any intuitive reason why this should be happening in my app and not in the minimal example? My app does nothing fancy, and in particular nothing that would cause this to fail, just a standard application of custom MainWindow and etc widgets.
wrote on 29 Nov 2019, 08:38 last edited by JonB@enjoysmath said in Draggable PyQt5 tabs in custom TabWidget work in min example, but not in practice:
This is purely a guess:globalPos = self.mapToGlobal(e.pos()) tabBar = self.tabBar() posInTab = tabBar.mapFromGlobal(globalPos) print(e.pos()) index = tabBar.tabAt(e.pos())
How do you know that the
globalPos
lies within thetabBar
? Look at the tab bar's rectangle and see if the mouse pos really does lie inside it? Similarly, are there any "gaps"/"areas" in the tab bar such that a mouse pos does not actually lie in a particular tab? I would debug out those position coordinates and compare to what's actually occupied by your tab bar/tabs, to understand what is going on. -
wrote on 29 Nov 2019, 17:06 last edited by
def mouseMoveEvent(self, e): if e.buttons() != Qt.RightButton: return globalPos = self.mapToGlobal(e.pos()) tabBar = self.tabBar() posInTab = tabBar.mapFromGlobal(globalPos) index = tabBar.tabAt(posInTab) tabBar.dragged_content = self.widget(index) tabBar.dragged_tabname = self.tabText(index) tabRect = tabBar.tabRect(index) pixmap = QPixmap(tabRect.size()) tabBar.render(pixmap,QPoint(),QRegion(tabRect)) mimeData = QMimeData() drag = QDrag(tabBar) drag.setMimeData(mimeData) drag.setPixmap(pixmap) cursor = QCursor(Qt.OpenHandCursor) drag.setHotSpot(posInTab) drag.setDragCursor(cursor.pixmap(),Qt.MoveAction) drag.exec_(Qt.MoveAction)
Replace code with this code. In particular we don't take e.pos() except once at the top, then we use globalPos for computing poosInTab, then drag.setHotSpot(posInTab). It works!
1/3