3

I am trying to drag a tab from a tab widget and split the viewing area (- like eclipse does then you drag a tab heading into the edit area - and similar to the 'split vertically' functionality on a tab in PyCharm).

I tried using drag and drop but can't get the tab main area (eg text area) to register a drag coming from the QTabBar. I then tried just following the mouse movements and got further but the splitter code is not quite working and it's pretty ugly code.

NB I'm not using dockable widgets as this code will be used for the central widget of an app. I'd prefer to use drag and drop to do this - anyone have any ideas?

from PyQt4.QtGui import QWidget, QDrag, QTabBar, QTabWidget, QPainter, QPalette,\
QBrush, QColor, QPen, QVBoxLayout, QHBoxLayout, QTextEdit, QCheckBox
from PyQt4.QtCore import QByteArray, QMimeData, QPoint
from PyQt4 import QtCore, QtGui

class CentralTabWidget(QTabWidget):

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

        tabBar = CentralTabBar(self)
        self.setTabBar(tabBar)
        self.addWidgets()

    def addWidgets(self):
        tab1 = QWidget()
        self.addTab(tab1, "Tab1")
        textArea = QTextEdit()
        textArea.setText("Text area 1")
        vBox = QVBoxLayout()
        vBox.addWidget(textArea)
        tab1.setLayout(vBox)
        tab2 = QWidget()
        self.addTab(tab2, "Tab2")
        textArea2 = QTextEdit()
        textArea2.setText("Text area 2")
        vBox2 = QVBoxLayout()
        vBox2.addWidget(textArea2)
        tab2.setLayout(vBox2)
        self.setAcceptDrops(True)
        self.verticalLineOverlay = Overlay(parent = self)
        self.verticalLineOverlay.hide()

    def dragEnterEvent(self, event):
        mimeData = event.mimeData()
        event.accept()

    def dragMoveEvent(self, event):
        print(">>dragMoveEvent()")

    def dropEvent(self, event):
        mimeData = event.mimeData()
        event.setDropAction(QtCore.Qt.MoveAction)
        event.accept()

    def resizeEvent(self, event):    
        self.verticalLineOverlay.resize(event.size())
        event.accept()

class CentralTabBar(QTabBar):   
    def __init__(self, parent=None):
        QTabBar.__init__(self, parent)
        self.parent = parent
        self.__drag_start_pos = QPoint()
        self.setAcceptDrops(True)

    def mousePressEvent(self, event):
        self.__mousePressPos = None
        self.__mouseMovePos = None
        if event.button() == QtCore.Qt.LeftButton:
            self.__mousePressPos = event.globalPos()
            self.__mouseMovePos = event.globalPos()
            if self.parent.parent.dndCheckBox.isChecked():
                self.startDrag()
        super(CentralTabBar, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        self.parent.verticalLineOverlay.setVisible(False)
        currPos = self.mapToGlobal(self.pos())
        ax, ay, aw, ah = self.geometry().getRect()
        if currPos.x() > ax + aw/4:
            self.parent.parent.createSplitter()
        super(CentralTabBar, self).mouseReleaseEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() == QtCore.Qt.LeftButton:
            currPos = self.mapToGlobal(self.pos())
            globalPos = event.globalPos()
            diff = globalPos - self.__mouseMovePos
            newPos = self.mapFromGlobal(currPos + diff)
            xp1, yp1, xp2, yp2 = self.geometry().getCoords()
            ax, ay, aw, ah = self.geometry().getRect()
            parentx, parenty, parentw, parenth = self.parent.geometry().getCoords()
            if parenth > 10:
                if (newPos.y() > yp2) and (newPos.y() < (parenth)):
                    self.parent.verticalLineOverlay.setVisible(True)
        super(CentralTabBar, self).mouseMoveEvent(event)

    def startDrag(self):
        data = QByteArray()
        mimeData = QMimeData()
        mimeData.setData("application/x-icon-and-text", data)
        print("Using DnD")
        drag = QDrag(self)
        drag.setMimeData(mimeData)
        drag.exec_()

    def dragEnterEvent(self, event):
        event.accept()

    def dragMoveEvent(self, event):
        print("--dragMoveEvent()")

    def dropEvent(self, event):
        event.setDropAction(QtCore.Qt.MoveAction)
        event.accept()

class Overlay(QWidget):    
    def __init__(self, parent=None):        
        super(Overlay, self).__init__(parent)
        print("--__init__() parent type:{0}".format(type(parent))) 
        self.parent = parent
        palette = QPalette(self.palette())
        palette.setColor(palette.Background, QtCore.Qt.transparent)
        self.setPalette(palette)

    def paintEvent(self, event):  
        self.painter = QPainter()
        self.painter.setPen(QPen(QtCore.Qt.NoPen))
        self.painter.begin(self)
        self.painter.setRenderHint(QPainter.Antialiasing)
        self.painter.fillRect(self.parent.currentWidget().geometry(), QBrush(QColor(255, 255, 255, 10)))
        parentx, parenty, parentw, parenth = self.parent.geometry().getCoords()
        self.painter.drawRect( parentx, parenty, (parentw/2)-10, parenth)   
        self.painter.drawRect( (parentw/2)+10, parenty, parentw, parenth)        
        self.painter.setPen(QPen(QtCore.Qt.NoPen)) 

    def setUpPainter(self):
        self.painter = QPainter()
        self.painter.setPen(QPen(QtCore.Qt.NoPen))
        self.painter.begin(self)
        self.painter.setRenderHint(QPainter.Antialiasing)
        self.painter.fillRect(self.parent.currentWidget().geometry(), QBrush(QColor(255, 255, 255, 10)))
        parentx, parenty, parentw, parenth = self.parent.geometry().getCoords()

class Window(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.layout = QHBoxLayout()
        self.dndCheckBox = QCheckBox("Use DnD")
        self.dndCheckBox.setChecked(True)
        self.ctw = CentralTabWidget(self)
        self.layout.addWidget(self.ctw)
        self.layout.addWidget(self.dndCheckBox)
        self.setLayout(self.layout)

    def createSplitter(self):
        #not sure why widgets are not redrawn inside the splitter
        splitter1 = QtGui.QSplitter(QtCore.Qt.Horizontal)
        self.removeWidgets(self.layout)
        splitter1.addWidget(self.ctw)
        self.layout.addWidget(splitter1)
        self.layout.addWidget(self.dndCheckBox)

    def removeWidgets(self, layout):
        for cnt in reversed(range(layout.count())):
            widget = layout.takeAt(cnt).widget()
            if widget is not None: 
                widget.deleteLater()

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.resize(350, 300)
    window.show()
    sys.exit(app.exec_())

Using a QMainWindow inside a QMainMaindow I tried:

from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import QMainWindow, QTextEdit, QDockWidget

_DOCK_OPTS = QtGui.QMainWindow.AllowNestedDocks
_DOCK_OPTS |= QtGui.QMainWindow.AllowTabbedDocks

class Window(QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        secondQMainWindow = QMainWindow()
        self.central = secondQMainWindow
        self.setDockOptions(_DOCK_OPTS)
        dw1 = QDockWidget("One")
        textArea = QTextEdit()
        textArea.setText("Text area 1")
        dw1.setWidget(textArea)

        dw2 = QDockWidget("Two")
        textArea2 = QTextEdit()
        textArea2.setText("Text area 2")
        dw2.setWidget(textArea2)
        self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dw1)
        self.addDockWidget(QtCore.Qt.RightDockWidgetArea, dw2)
        self.tabifyDockWidget(dw1, dw2)
        dw3 = QDockWidget("Three")
        textArea3 = QTextEdit()
        textArea3.setText("Text area 3")
        dw3.setWidget(textArea3)
        self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dw3)

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.show()
    app.exec_()

But I don't think I have the addition of widgets to the parent vs child central widget correct.

Don Smythe
  • 9,234
  • 14
  • 62
  • 105

2 Answers2

4

Here is a trick I've used in the past to do what you are looking for. It may be quite limited if you want to have a lot of control on the parameters but it does the job.

  1. Put a 2nd QMainWindow as the centralWidget of you QMainWindow.
  2. Don't set any centralWidget for this 2nd QMainWindow.
  3. Set flags : QMainWindow.AllowNestedDocks and QMainWindow.AllowTabbedDocks

  4. Add tabs by adding QDockWidgets. These will be automatically threated as QTabWidgets.

  5. Use QMainWindow.tabifyDockWidget(firstWidget,secondWidget) to stack the widgets programmatically (it can be used multiple times for more than 2 widgets)

Example :

window=QtGui.QMainWindow()
    window.centralContent=QtGui.QMainWindow()

window.setCentralWidget(window.centralContent)

window.centralContent.firstTabWidget=QtGui.QWidget()
window.centralContent.firstTabDock=QtGui.QDockWidget("first")
window.centralContent.firstTabDock.setWidget(window.centralContent.firstTabWidget)
window.centralContent.addDockWidget(window.centralContent.firstTabDock)
 window.centralContent.secondTabWidget=QtGui.QWidget()
window.centralContent.secondTabDock=QtGui.QDockWidget("second")
window.centralContent.secondTabDock.setWidget(window.centralContent.secondTabWidget)
window.centralContent.addDockWidget(window.centralContent.secondTabDock)

window.centralContent.tabifyDockWidget( window.centralContent.firstTabDock, window.centralContent.secondTabDock)
Olivier Giniaux
  • 870
  • 2
  • 8
  • 22
  • I have added an attempt at your suggestion to the original question. I don't have the code right - do you mean add the 'firstwidget' and 'secondWidget' to the 2nd main window? - I tried that but got a blank dialog. – Don Smythe Aug 10 '15 at 11:43
  • I've written a little exemple. I couldn't test it as I wrote it from m'y phone. It should work it is vert close to what I did – Olivier Giniaux Aug 10 '15 at 12:26
  • I'd give you a second point if I could for writing this on your phone – Don Smythe Aug 10 '15 at 12:42
  • 1
    +1 for writing the answer from your phone (And, of course, it helped me) – automaton Sep 02 '16 at 15:45
1

Supports both tab-to-tab and tab-to-other-window's tabs

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)
      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_())
MathCrackExchange
  • 595
  • 1
  • 6
  • 25
  • Doesn't work for me on W10; I'll test it in Linux too. Displays two QTabWidgets side-by-side, tabs appear to be draggable, but can't be rearanged within the QMainWindow or outside. EDIT: Ah - it works with the RIGHT click and drag! But only between the two containers in the window, no luck with tab-to-other-window. – Oak_3260548 Jul 14 '20 at 12:50
  • @Oak_3260548 idk, when I was working on that code it was working for me on win 10... – MathCrackExchange Jul 14 '20 at 19:00
  • thank you for assistance. I am I right to asume, that one can drag tabs only between existing QTabWidgets? So no chance to create a new panel or window by dragging the tab of existing QTabWidget(s), right? – Oak_3260548 Jul 15 '20 at 05:52