从QTabWidget中拖放选项卡

3
我正在尝试从选项卡窗口中拖动一个选项卡并分割视图区域(-就像Eclipse在将选项卡标题拖入编辑区域时一样-类似于PyCharm选项卡上的“垂直分割”功能)。
我尝试使用拖放,但无法使选项卡主区域(例如文本区域)注册来自QTabBar的拖动。然后我尝试跟随鼠标移动并取得了进展,但是分隔符代码还不太起作用,并且代码相当丑陋。
请注意,我没有使用可停靠小部件,因为此代码将用于应用程序的中心小部件。我希望使用拖放来完成这项工作-有任何想法吗?
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_())

在一个QMainWindow中使用另一个QMainWindow,我尝试过:

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_()

但我不认为我对父控件和子控件中央控件的小部件添加有正确的理解。


我将第二个代码框中的代码移植到了PyQt5,并且看起来非常好用! - Oak_3260548
2个回答

4
以下是我过去使用的一个技巧,可以实现你所需求的功能。如果你想对参数进行更多控制,则此方法可能会受到限制,但它能够完成工作。
  1. 将第二个 QMainWindow 设置为你的 QMainWindowcentralWidget
  2. 不要为这个第二个 QMainWindow 设置任何 centralWidget
  3. 设置标志:QMainWindow.AllowNestedDocksQMainWindow.AllowTabbedDocks

  4. 通过添加 QDockWidgets 添加选项卡。这些将自动处理为 QTabWidgets

  5. 使用 QMainWindow.tabifyDockWidget(firstWidget,secondWidget) 以编程方式堆叠小部件(可用于多个小部件)

示例:

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)

我已经尝试着按照你的建议修改了原问题。但是我没有正确的代码 - 你是指将'firstwidget'和'secondWidget'添加到第二个主窗口中吗?- 我尝试过了,但是得到了一个空白对话框。 - Don Smythe
我写了一个小例子。由于我是用手机编写的,所以无法测试它。它应该可以工作,因为它非常接近我所做的事情。 - Olivier Giniaux
如果可以的话,我会再给你一个点赞,因为你是在手机上写的。 - Don Smythe
1
+1 是指在社交媒体或论坛中表示赞同的意思,而这句话的意思是“+1 对我很有帮助”。 - automaton

1
支持切换到当前标签页和其他窗口的标签页。
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_())

在我的W10上无法工作;我也会在Linux上测试它。将两个QTabWidgets并排显示,选项卡似乎可以拖动,但无法在QMainWindow内或外重新排列。编辑:啊——用右键单击和拖动可以工作!但仅限于窗口中的两个容器之间,将选项卡拖到其他窗口没有成功。 - Oak_3260548
@Oak_3260548 我不知道,当我在那段代码上工作时,在win 10上它对我是有效的... - D Left Adjoint to U
感谢您的帮助。我可以假设,只能在现有的QTabWidgets之间拖动选项卡吗?因此,无法通过拖动现有QTabWidget的选项卡来创建新面板或窗口,对吗? - Oak_3260548

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接