Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Drag & drop between widgets in a Layout



  • Hi everyone.

    I have a project (using Qt.py) where I can drag and drop widgets between layouts (mainly VBox & HBox). All I have at the moment is the ability drag one widget onto another and it just reparents itself to the containing widget and adds it to the bottom.

    What I'm looking for is the ability to drop between widgets, and for a visible line to indicate where it will drop, similar to a QListWidget (or if you were dragging widgets around inside Qt Designer.).

    This is the basics of one of my classes that's just acting as a wrapper to a VBoxLayout:

    from Qt import QtCore, QtWidgets, QtGui
    
    class DragDropPanel(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(DragDropPanel, self).__init__(parent=parent)
            self.setAcceptDrops(True)
            self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
            self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
    
            primaryLayout = QtWidgets.QVBoxLayout()
            primaryLayout.setAlignment(QtCore.Qt.AlignTop)
            self.setLayout(primaryLayout)
    
        def dragEnterEvent(self, event):
            source = event.source()
            if not source == self:
                event.accept()
            else:
                event.ignore()
    
        def dropEvent(self, event):
            source = event.source()
            if not source == self:
                # I have no idea what the correct way to add/remove widgets from one layout
                # to another is :( this seems to work though.
                source.setParent(self)  # Is this needed? or the call to addWidget will do this for me?
                self.layout().addWidget(source)
                event.accept()
            else:
                event.ignore()
    
        def mouseMoveEvent(self, event):
            mimeData = QtCore.QMimeData()
            dragPixmap = self.grab()
    
            drag = QtGui.QDrag(self)
            drag.setMimeData(mimeData)
            drag.setPixmap(dragPixmap)
            drag.setHotSpot(event.pos())
    
            self.setParent(None)
    
            drag.exec_(QtCore.Qt.CopyAction | QtCore.Qt.MoveAction)
    

    So as you can see, if you added multiple of these to a window, you would be able to drag and drop them inside of each other, creating nested hierarchies or whatever you wanted. But it will always add it to the bottom as I'm just calling addWidget.

    Is there a way where I can determine which position I'm at and draw a line between each widget as I hover over the position? I've only thought of trivial solutions such as replacing the 'spacing' in the layout with invisible widgets where I can make use of their drag enter and drop events, but this just feels like a hack. I'm still fairly new to Qt :(

    I don't mind what language help is posted in either :)

    Thank you! (Also feel free to point out bad qt practices)


  • Banned

    Okay not sure if this will help any but here is something similar that I ran across. I would have just posted the URL but cannot seem to locate that so posting the source code for it instead. Note it consists of 5 separate files so I have included each one individually in its own post as too long for one post. Hope this helps give ideas of perhaps how to do what you are trying to do
    PyQtGraphDocks.py contents

    # Declaring this first forces pyqtgraph to use PyQt5
    from PyQt5.QtCore    import QObject, pyqtSlot
    from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton
    
    from sys   import exit     as sysExit
    
    from numpy import random   as npRndm
    
    import pyqtgraph as pqGrph
    from pyqtgraph.console.Console import ConsoleWidget as pgcwConsol
    
    from DockArea import DockArea
    from Dock     import Dock as DockIt
    ''' -- Dock Area Example --
        This window has 6 Dock widgets in it. Each dock can be dragged
        by its title bar to occupy a different space within the window 
        but note that one dock has its title bar hidden). Additionally,
        the borders between docks may be dragged to resize. Docks that 
        are dragged on top of one another are stacked in a tabbed layout. 
        Double-click a dock title bar to place it in its own window.
    '''
    class Graphables(QObject):
        def __init__(self, parent):
            QObject.__init__(self)
            self.Parnt = parent
    
        def SetGrphArea(self):
            self.SavdState = None
    
          # Create widgets to add to Dock 1
            self.lblHeader = QLabel('-- Dock Area Example --')
    
            self.btnSave = QPushButton('Save')
            self.btnSave.clicked.connect(self.SavState)
    
            self.btnRstr = QPushButton('Restore')
            self.btnRstr.clicked.connect(self.LodState)
            self.btnRstr.setEnabled(False)
    
          # First dock gets the save/restore buttons
            self.Wdgt1 = pqGrph.LayoutWidget()
            self.Wdgt1.addWidget(self.lblHeader, row=0, col=0)
            self.Wdgt1.addWidget(self.btnSave,   row=1, col=0)
            self.Wdgt1.addWidget(self.btnRstr,   row=2, col=0)
    
          # Set this dock to the smallest size possible
            self.Dock1 = DockIt("Dock1", size=(1, 1))     
            self.Dock1.addWidget(self.Wdgt1)
    
          # Console Widget
            self.Wdgt2 = pgcwConsol()
    
            self.Dock2 = DockIt("Dock2 - Console", size=(500,300))
            self.Dock2.addWidget(self.Wdgt2)
    
            self.Wdgt3 = pqGrph.PlotWidget(title="Plot inside dock with no title bar")
            self.Wdgt3.plot(npRndm.normal(size=100))
    
          # Hide the title bar on dock 3
            self.Dock3 = DockIt("Dock3", size=(500,400))
            self.Dock3.hideTitleBar()
            self.Dock3.addWidget(self.Wdgt3)
    
          # -------
            self.Wdgt4 = pqGrph.PlotWidget(title="Dock 4 Plot")
            self.Wdgt4.plot(npRndm.normal(size=100))
    
            self.Dock4 = DockIt("Dock4 (tabbed) - Plot", size=(500,200))
            self.Dock4.addWidget(self.Wdgt4)
    
          # -------
            self.Wdgt5 = pqGrph.ImageView()
            self.Wdgt5.setImage(npRndm.normal(size=(100,100)))
    
            self.Dock5 = DockIt("Dock5 - Image", size=(500,200))
            self.Dock5.addWidget(self.Wdgt5)
    
          # -------
            self.Wdgt6 = pqGrph.PlotWidget(title="Dock 6 Plot")
            self.Wdgt6.plot(npRndm.normal(size=100))
    
            self.Dock6 = DockIt("Dock6 (tabbed) - Plot", size=(500,200))
            self.Dock6.addWidget(self.Wdgt6)
    
            ## Take the docks and place them into the window one at a time.
            ## Note that size arguments are only a suggestion; docks will still have to
            ## fill the entire dock area and obey the limits of their internal widgets.
    # This is located in the Central Widget in QMainWindow Init
            self.Parnt.GrphArea.addDock(self.Dock1, 'left')      ## place d1 at left edge of dock area (it will fill the whole space since there are no other docks yet)
            self.Parnt.GrphArea.addDock(self.Dock2, 'right')     ## place d2 at right edge of dock area
            self.Parnt.GrphArea.addDock(self.Dock3, 'bottom', self.Dock1)## place d3 at bottom edge of d1
            self.Parnt.GrphArea.addDock(self.Dock4, 'right')     ## place d4 at right edge of dock area
            self.Parnt.GrphArea.addDock(self.Dock5, 'left', self.Dock1)  ## place d5 at left edge of d1
            self.Parnt.GrphArea.addDock(self.Dock6, 'top', self.Dock4)   ## place d5 at top edge of d4
    
            ## Test ability to move docks programatically after they have been placed
            self.Parnt.GrphArea.moveDock(self.Dock4, 'top', self.Dock2)     ## move d4 to top edge of d2
            self.Parnt.GrphArea.moveDock(self.Dock6, 'above', self.Dock4)   ## move d6 to stack on top of d4
            self.Parnt.GrphArea.moveDock(self.Dock5, 'top', self.Dock2)     ## move d5 to top edge of d2
    
        @pyqtSlot()
        def SavState():
            self.SavdState = self.Parnt.GrphArea.saveState()
            self.btnRstr.setEnabled(True)
    
        @pyqtSlot()
        def LodState():
            self.Parnt.GrphArea.btnRstr(self.SavdState)
    
    class Window(QMainWindow):
        def __init__(self):
            QMainWindow.__init__(self)
            self.resize(1000,500)
            self.setWindowTitle('Graph Docking Area')
    
            self.Grph = Graphables(self)
            self.GrphArea = DockArea()
            self.Grph.SetGrphArea()
            self.setCentralWidget(self.GrphArea)
    
    if __name__ == '__main__':
        MainThred = QApplication([])
        
        MainApp = Window()
        MainApp.show()
    
        sysExit(MainThred.exec_())
    
      # If anyone wants more extensive free help I run an online lab-like classroom-like 
      # message server feel free and drop by you will not be able to post until I clear 
      # you as a student as this prevents spammers so if interested here is the invite
      # https://discord.gg/3D8huKC
    

  • Banned

    DockArea.py contents

    from PyQt5.QtWidgets import QWidget, QMainWindow, QVBoxLayout
    
    import numpy     as np
    import random    as rndm
    import pyqtgraph as pqGrph
    
    from sys     import exit   as sysExit
    from numpy   import random as npRndm
    from weakref import WeakValueDictionary as WeakDict
    
    # Non-Python Non-PyQt Classes
    from Container import Container  as Contnr
    from Container import HContainer as HCntnr
    from Container import VContainer as VCntnr
    from Container import TContainer as TCntnr
    from Container import SplitContainer
    
    from Dock      import Dock as DockIt
    from DockDrop  import DockDrop
    
    class DockArea(Contnr, QWidget, DockDrop):
        def __init__(self, temporary=False, home=None):
            Contnr.__init__(self, self)
            QWidget.__init__(self)
            DockDrop.__init__(self, allowedAreas=['left', 'right', 'top', 'bottom'])
    
            self.layout = QVBoxLayout()
            self.layout.setContentsMargins(0,0,0,0)
            self.layout.setSpacing(0)
            self.setLayout(self.layout)
            self.docks = WeakDict()
            self.topContainer = None
            self.raiseOverlay()
            self.temporary = temporary
            self.tempAreas = []
            self.home = home
    
        def type(self):
            return "top"
            
        def addDock(self, dock=None, position='bottom', relativeTo=None, **kwds):
            """Adds a dock to this area.
            
            ============== =================================================================
            **Arguments:**
            dock           The new Dock object to add. If None, then a new Dock will be 
                           created.
            position       'bottom', 'top', 'left', 'right', 'above', or 'below'
            relativeTo     If relativeTo is None, then the new Dock is added to fill an 
                           entire edge of the window. If relativeTo is another Dock, then 
                           the new Dock is placed adjacent to it (or in a tabbed 
                           configuration for 'above' and 'below'). 
            ============== =================================================================
            
            All extra keyword arguments are passed to Dock.__init__() if *dock* is
            None.        
            """
            if dock is None:
                dock = DockIt(**kwds)
            
            ## Determine the container to insert this dock into.
            ## If there is no neighbor, then the container is the top.
            if relativeTo is None or relativeTo is self:
                if self.topContainer is None:
                    CurntContnr = self
                    neighbor = None
                else:
                    CurntContnr = self.topContainer
                    neighbor = None
            else:
                if isinstance(relativeTo, str):
                    relativeTo = self.docks[relativeTo]
                CurntContnr = self.getContainer(relativeTo)
                neighbor = relativeTo
            
            ## what container type do we need?
            neededContainer = {
                'bottom': 'vertical',
                'top': 'vertical',
                'left': 'horizontal',
                'right': 'horizontal',
                'above': 'tab',
                'below': 'tab'
            }[position]
            
            ## Can't insert new containers into a tab container; insert outside instead.
            if neededContainer != CurntContnr.type() and CurntContnr.type() == 'tab':
                neighbor = CurntContnr
                CurntContnr = Contnr()
                
            ## Decide if the container we have is suitable.
            ## If not, insert a new container inside.
            if neededContainer != CurntContnr.type():
                if neighbor is None:
                    CurntContnr = self.addContainer(neededContainer, self.topContainer)
                else:
                    CurntContnr = self.addContainer(neededContainer, neighbor)
                
            ## Insert the new dock before/after its neighbor
            insertPos = {
                'bottom': 'after',
                'top': 'before',
                'left': 'before',
                'right': 'after',
                'above': 'before',
                'below': 'after'
            }[position]
            #print "request insert", dock, insertPos, neighbor
            old = dock.container()
            CurntContnr.insert(dock, insertPos, neighbor)
            dock.area = self
            self.docks[dock.name()] = dock
            if old is not None:
                old.apoptose()
            
            return dock
            
        def moveDock(self, dock, position, neighbor):
            """
            Move an existing Dock to a new location. 
            """
            ## Moving to the edge of a tabbed dock causes a drop outside the tab box
            if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab':
                neighbor = neighbor.container()
            self.addDock(dock, position, neighbor)
            
        def getContainer(self, obj):
            if obj is None:
                return self
            return obj.container()
            
        def makeContainer(self, typ):
            if typ == 'vertical':
                new = VCntnr(self)
            elif typ == 'horizontal':
                new = HCntnr(self)
            elif typ == 'tab':
                new = TCntnr(self)
            return new
            
        def addContainer(self, typ, obj):
            """Add a new container around obj"""
            new = self.makeContainer(typ)
            
            container = self.getContainer(obj)
            container.insert(new, 'before', obj)
            #print "Add container:", new, " -> ", container
            if obj is not None:
                new.insert(obj)
            self.raiseOverlay()
            return new
        
        def insert(self, new, pos=None, neighbor=None):
            if self.topContainer is not None:
                self.topContainer.containerChanged(None)
            self.layout.addWidget(new)
            self.topContainer = new
            #print self, "set top:", new
            new._container = self
            self.raiseOverlay()
            #print "Insert top:", new
            
        def count(self):
            if self.topContainer is None:
                return 0
            return 1
            
        #def paintEvent(self, ev):
            #self.drawDockOverlay()
            
        def resizeEvent(self, ev):
            self.resizeOverlay(self.size())
            
        def addTempArea(self):
            if self.home is None:
                area = DockArea(temporary=True, home=self)
                self.tempAreas.append(area)
                win = TempAreaWindow(area)
                area.win = win
                win.show()
            else:
                area = self.home.addTempArea()
            #print "added temp area", area, area.window()
            return area
            
        def floatDock(self, dock):
            """Removes *dock* from this DockArea and places it in a new window."""
            area = self.addTempArea()
            area.win.resize(dock.size())
            area.moveDock(dock, 'top', None)
            
        def removeTempArea(self, area):
            self.tempAreas.remove(area)
            #print "close window", area.window()
            area.window().close()
            
        def saveState(self):
            """
            Return a serialized (storable) representation of the state of
            all Docks in this DockArea."""
    
            if self.topContainer is None:
                main = None
            else:
                main = self.childState(self.topContainer)
    
            state = {'main': main, 'float': []}
            for a in self.tempAreas:
                geo = a.win.geometry()
                geo = (geo.x(), geo.y(), geo.width(), geo.height())
                state['float'].append((a.saveState(), geo))
            return state
            
        def childState(self, obj):
            if isinstance(obj, DockIt):
                return ('dock', obj.name(), {})
            else:
                childs = []
                for i in range(obj.count()):
                    childs.append(self.childState(obj.widget(i)))
                return (obj.type(), childs, obj.saveState())
            
        def restoreState(self, state):
            """
            Restore Dock configuration as generated by saveState.
            
            Note that this function does not create any Docks--it will only 
            restore the arrangement of an existing set of Docks.
            
            """
            
            ## 1) make dict of all docks and list of existing containers
            containers, docks = self.findAll()
            oldTemps = self.tempAreas[:]
            #print "found docks:", docks
            
            ## 2) create container structure, move docks into new containers
            if state['main'] is not None:
                self.buildFromState(state['main'], docks, self)
            
            ## 3) create floating areas, populate
            for s in state['float']:
                a = self.addTempArea()
                a.buildFromState(s[0]['main'], docks, a)
                a.win.setGeometry(*s[1])
            
            ## 4) Add any remaining docks to the bottom
            for d in docks.values():
                self.moveDock(d, 'below', None)
            
            #print "\nKill old containers:"
            ## 5) kill old containers
            for c in containers:
                c.close()
            for a in oldTemps:
                a.apoptose()
    
        def buildFromState(self, state, docks, root, depth=0):
            typ, contents, state = state
            pfx = "  " * depth
            if typ == 'dock':
                try:
                    obj = docks[contents]
                    del docks[contents]
                except KeyError:
                    raise Exception('Cannot restore dock state; no dock with name "%s"' % contents)
            else:
                obj = self.makeContainer(typ)
                
            root.insert(obj, 'after')
            #print pfx+"Add:", obj, " -> ", root
            
            if typ != 'dock':
                for o in contents:
                    self.buildFromState(o, docks, obj, depth+1)
                obj.apoptose(propagate=False)
                obj.restoreState(state)  ## this has to be done later?
            
        def findAll(self, obj=None, c=None, d=None):
            if obj is None:
                obj = self.topContainer
            
            ## check all temp areas first
            if c is None:
                c = []
                d = {}
                for a in self.tempAreas:
                    c1, d1 = a.findAll()
                    c.extend(c1)
                    d.update(d1)
            
            if isinstance(obj, DockIt):
                d[obj.name()] = obj
            elif obj is not None:
                c.append(obj)
                for i in range(obj.count()):
                    o2 = obj.widget(i)
                    c2, d2 = self.findAll(o2)
                    c.extend(c2)
                    d.update(d2)
            return (c, d)
    
        def apoptose(self):
            #print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count()
            if self.topContainer.count() == 0:
                self.topContainer = None
                if self.temporary:
                    self.home.removeTempArea(self)
                    #self.close()
    
        def clear(self):
            docks = self.findAll()[1]
            for dock in docks.values():
                dock.close()
                
        ## PySide bug: We need to explicitly redefine these methods
        ## or else drag/drop events will not be delivered.
        def dragEnterEvent(self, *args):
            DockDrop.dragEnterEvent(self, *args)
    
        def dragMoveEvent(self, *args):
            DockDrop.dragMoveEvent(self, *args)
    
        def dragLeaveEvent(self, *args):
            DockDrop.dragLeaveEvent(self, *args)
    
        def dropEvent(self, *args):
            DockDrop.dropEvent(self, *args)
    
    class TempAreaWindow(QMainWindow):
        def __init__(self, area, **kwargs):
            QMainWindow.__init__(self, **kwargs)
            self.setCentralWidget(area)
    
        def closeEvent(self, *args, **kwargs):
            self.centralWidget().clear()
            QMainWindow.closeEvent(self, *args, **kwargs)
    
      # If anyone wants more extensive free help I run an online lab-like classroom-like 
      # message server feel free and drop by you will not be able to post until I clear 
      # you as a student as this prevents spammers so if interested here is the invite
      # https://discord.gg/3D8huKC
    

  • Banned

    Dock.py contents

    from PyQt5.QtCore    import Qt, QMimeData, pyqtSignal
    from PyQt5.QtGui     import QDrag
    from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QSizePolicy
    
    from DockDrop  import DockDrop
    from pyqtgraph.widgets.VerticalLabel import VerticalLabel
    
    class Dock(QWidget, DockDrop):
        
        sigStretchChanged = pyqtSignal()
        
        def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True):
            QWidget.__init__(self)
            DockDrop.__init__(self)
            self._container = None
            self.area = area
            self.label = DockLabel(name, self)
            self.labelHidden = False
            self.moveLabel = True  ## If false, the dock is no longer allowed to move the label.
            self.autoOrient = autoOrientation
            self.orientation = 'horizontal'
            self.topLayout = QGridLayout()
            self.topLayout.setContentsMargins(0, 0, 0, 0)
            self.topLayout.setSpacing(0)
            self.setLayout(self.topLayout)
            self.topLayout.addWidget(self.label, 0, 1)
            self.widgetArea = QWidget()
            self.topLayout.addWidget(self.widgetArea, 1, 1)
            self.layout = QGridLayout()
            self.layout.setContentsMargins(0, 0, 0, 0)
            self.layout.setSpacing(0)
            self.widgetArea.setLayout(self.layout)
            self.widgetArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
            self.widgets = []
            self.currentRow = 0
            self.raiseOverlay()
            self.hStyle = """
            Dock > QWidget { 
                border: 1px solid #000; 
                border-radius: 5px; 
                border-top-left-radius: 0px; 
                border-top-right-radius: 0px; 
                border-top-width: 0px;
            }"""
            self.vStyle = """
            Dock > QWidget { 
                border: 1px solid #000; 
                border-radius: 5px; 
                border-top-left-radius: 0px; 
                border-bottom-left-radius: 0px; 
                border-left-width: 0px;
            }"""
            self.nStyle = """
            Dock > QWidget { 
                border: 1px solid #000; 
                border-radius: 5px; 
            }"""
            self.dragStyle = """
            Dock > QWidget { 
                border: 4px solid #00F; 
                border-radius: 5px; 
            }"""
            self.setAutoFillBackground(False)
            self.widgetArea.setStyleSheet(self.hStyle)
            
            self.setStretch(*size)
            
            if widget is not None:
                self.addWidget(widget)
    
            if hideTitle:
                self.hideTitleBar()
    
        def implements(self, name=None):
            if name is None:
                return ['dock']
            else:
                return name == 'dock'
            
        def setStretch(self, x=None, y=None):
            """
            Set the 'target' size for this Dock. 
            The actual size will be determined by comparing this Dock's
            stretch value to the rest of the docks it shares space with.
            """
            if x is None:
                x = 0
            if y is None:
                y = 0
            self._stretch = (x, y)
            self.sigStretchChanged.emit()
            
        def stretch(self):
            return self._stretch
            
    
        def hideTitleBar(self):
            """
            Hide the title bar for this Dock.
            This will prevent the Dock being moved by the user.
            """
            self.label.hide()
            self.labelHidden = True
            if 'center' in self.allowedAreas:
                self.allowedAreas.remove('center')
            self.updateStyle()
            
        def showTitleBar(self):
            """
            Show the title bar for this Dock.
            """
            self.label.show()
            self.labelHidden = False
            self.allowedAreas.add('center')
            self.updateStyle()
            
        def setOrientation(self, o='auto', force=False):
            """
            Sets the orientation of the title bar for this Dock.
            Must be one of 'auto', 'horizontal', or 'vertical'.
            By default ('auto'), the orientation is determined
            based on the aspect ratio of the Dock.        
            """
            if o == 'auto' and self.autoOrient:
                if self.container().type() == 'tab':
                    o = 'horizontal'
                elif self.width() > self.height()*1.5:
                    o = 'vertical'
                else:
                    o = 'horizontal'
            if force or self.orientation != o:
                self.orientation = o
                self.label.setOrientation(o)
                self.updateStyle()
            
        def updateStyle(self):
          # updates orientation and appearance of title bar
            if self.labelHidden:
                self.widgetArea.setStyleSheet(self.nStyle)
            elif self.orientation == 'vertical':
                self.label.setOrientation('vertical')
                if self.moveLabel:
                    self.topLayout.addWidget(self.label, 1, 0)
                self.widgetArea.setStyleSheet(self.vStyle)
            else:
                self.label.setOrientation('horizontal')
                if self.moveLabel:
                    self.topLayout.addWidget(self.label, 0, 1)
                self.widgetArea.setStyleSheet(self.hStyle)
    
        def resizeEvent(self, ev):
            self.setOrientation()
            self.resizeOverlay(self.size())
    
        def name(self):
            return str(self.label.text())
    
        def container(self):
            return self._container
    
        def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1):
            """
            Add a new widget to the interior of this Dock. 
            Each Dock uses a QGridLayout to arrange widgets within.
            """
            if row is None:
                row = self.currentRow
            self.currentRow = max(row+1, self.currentRow)
            self.widgets.append(widget)
            self.layout.addWidget(widget, row, col, rowspan, colspan)
            self.raiseOverlay()
            
            
        def startDrag(self):
            self.drag = QDrag(self)
            mime = QMimeData()
            #mime.setPlainText("asd")
            self.drag.setMimeData(mime)
            self.widgetArea.setStyleSheet(self.dragStyle)
            self.update()
            action = self.drag.exec_()
            self.updateStyle()
            
        def float(self):
            self.area.floatDock(self)
                
        def containerChanged(self, c):
            #print self.name(), "container changed"
            self._container = c
            if c.type() != 'tab':
                self.moveLabel = True
                self.label.setDim(False)
            else:
                self.moveLabel = False
                
            self.setOrientation(force=True)
    
        def close(self):
            """Remove this dock from the DockArea it lives inside."""
            self.setParent(None)
            self.label.setParent(None)
            self._container.apoptose()
            self._container = None
    
        def __repr__(self):
            return "<Dock %s %s>" % (self.name(), self.stretch())
    
        ## PySide bug: We need to explicitly redefine these methods
        ## or else drag/drop events will not be delivered.
        def dragEnterEvent(self, *args):
            DockDrop.dragEnterEvent(self, *args)
    
        def dragMoveEvent(self, *args):
            DockDrop.dragMoveEvent(self, *args)
    
        def dragLeaveEvent(self, *args):
            DockDrop.dragLeaveEvent(self, *args)
    
        def dropEvent(self, *args):
            DockDrop.dropEvent(self, *args)
    
    class DockLabel(VerticalLabel):
        
        sigClicked = pyqtSignal(object, object)
        
        def __init__(self, text, dock):
            self.dim = False
            self.fixedWidth = False
            VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False)
            self.setAlignment(Qt.AlignTop|Qt.AlignHCenter)
            self.dock = dock
            self.updateStyle()
            self.setAutoFillBackground(False)
    
        def updateStyle(self):
            r = '3px'
            if self.dim:
                fg = '#aaa'
                bg = '#44a'
                border = '#339'
            else:
                fg = '#fff'
                bg = '#66c'
                border = '#55B'
            
            if self.orientation == 'vertical':
                self.vStyle = """DockLabel { 
                    background-color : %s; 
                    color : %s; 
                    border-top-right-radius: 0px; 
                    border-top-left-radius: %s; 
                    border-bottom-right-radius: 0px; 
                    border-bottom-left-radius: %s; 
                    border-width: 0px; 
                    border-right: 2px solid %s;
                    padding-top: 3px;
                    padding-bottom: 3px;
                }""" % (bg, fg, r, r, border)
                self.setStyleSheet(self.vStyle)
            else:
                self.hStyle = """DockLabel { 
                    background-color : %s; 
                    color : %s; 
                    border-top-right-radius: %s; 
                    border-top-left-radius: %s; 
                    border-bottom-right-radius: 0px; 
                    border-bottom-left-radius: 0px; 
                    border-width: 0px; 
                    border-bottom: 2px solid %s;
                    padding-left: 3px;
                    padding-right: 3px;
                }""" % (bg, fg, r, r, border)
                self.setStyleSheet(self.hStyle)
    
        def setDim(self, d):
            if self.dim != d:
                self.dim = d
                self.updateStyle()
        
        def setOrientation(self, o):
            VerticalLabel.setOrientation(self, o)
            self.updateStyle()
    
        def mousePressEvent(self, ev):
            if ev.button() == Qt.LeftButton:
                self.pressPos = ev.pos()
                self.startedDrag = False
                ev.accept()
            
        def mouseMoveEvent(self, ev):
            if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QApplication.startDragDistance():
                self.dock.startDrag()
            ev.accept()
            #print ev.pos()
                
        def mouseReleaseEvent(self, ev):
            if not self.startedDrag:
                self.sigClicked.emit(self, ev)
            ev.accept()
            
        def mouseDoubleClickEvent(self, ev):
            if ev.button() == Qt.LeftButton:
                self.dock.float()
    
      # If anyone wants more extensive free help I run an online lab-like classroom-like 
      # message server feel free and drop by you will not be able to post until I clear 
      # you as a student as this prevents spammers so if interested here is the invite
      # https://discord.gg/3D8huKC
    

  • Banned

    DockDrop.py contents

    from PyQt5.QtCore    import Qt, QRect
    from PyQt5.QtGui     import QPainter, QBrush, QColor, QPen
    from PyQt5.QtWidgets import QWidget
    
    class DockDrop(object):
        """Provides dock-dropping methods"""
        def __init__(self, allowedAreas=None):
            object.__init__(self)
            if allowedAreas is None:
                allowedAreas = ['center', 'right', 'left', 'top', 'bottom']
            self.allowedAreas = set(allowedAreas)
            self.setAcceptDrops(True)
            self.dropArea = None
            self.overlay = DropAreaOverlay(self)
            self.overlay.raise_()
        
        def resizeOverlay(self, size):
            self.overlay.resize(size)
            
        def raiseOverlay(self):
            self.overlay.raise_()
        
        def dragEnterEvent(self, ev):
            src = ev.source()
            if hasattr(src, 'implements') and src.implements('dock'):
                #print "drag enter accept"
                ev.accept()
            else:
                #print "drag enter ignore"
                ev.ignore()
            
        def dragMoveEvent(self, ev):
            #print "drag move"
            ld = ev.pos().x()
            rd = self.width() - ld
            td = ev.pos().y()
            bd = self.height() - td
            
            mn = min(ld, rd, td, bd)
            if mn > 30:
                self.dropArea = "center"
            elif (ld == mn or td == mn) and mn > self.height()/3.:
                self.dropArea = "center"
            elif (rd == mn or ld == mn) and mn > self.width()/3.:
                self.dropArea = "center"
                
            elif rd == mn:
                self.dropArea = "right"
            elif ld == mn:
                self.dropArea = "left"
            elif td == mn:
                self.dropArea = "top"
            elif bd == mn:
                self.dropArea = "bottom"
                
            if ev.source() is self and self.dropArea == 'center':
                #print "  no self-center"
                self.dropArea = None
                ev.ignore()
            elif self.dropArea not in self.allowedAreas:
                #print "  not allowed"
                self.dropArea = None
                ev.ignore()
            else:
                #print "  ok"
                ev.accept()
            self.overlay.setDropArea(self.dropArea)
                
        def dragLeaveEvent(self, ev):
            self.dropArea = None
            self.overlay.setDropArea(self.dropArea)
        
        def dropEvent(self, ev):
            area = self.dropArea
            if area is None:
                return
            if area == 'center':
                area = 'above'
            self.area.moveDock(ev.source(), area, self)
            self.dropArea = None
            self.overlay.setDropArea(self.dropArea)
    
            
    
    class DropAreaOverlay(QWidget):
        """Overlay widget that draws drop areas during a drag-drop operation"""
        
        def __init__(self, parent):
            QWidget.__init__(self, parent)
            self.dropArea = None
            self.hide()
            self.setAttribute(Qt.WA_TransparentForMouseEvents)
            
        def setDropArea(self, area):
            self.dropArea = area
            if area is None:
                self.hide()
            else:
                ## Resize overlay to just the region where drop area should be displayed.
                ## This works around a Qt bug--can't display transparent widgets over QGLWidget
                prgn = self.parent().rect()
                rgn = QRect(prgn)
                w = min(30, prgn.width()/3.)
                h = min(30, prgn.height()/3.)
                
                if self.dropArea == 'left':
                    rgn.setWidth(w)
                elif self.dropArea == 'right':
                    rgn.setLeft(rgn.left() + prgn.width() - w)
                elif self.dropArea == 'top':
                    rgn.setHeight(h)
                elif self.dropArea == 'bottom':
                    rgn.setTop(rgn.top() + prgn.height() - h)
                elif self.dropArea == 'center':
                    rgn.adjust(w, h, -w, -h)
                self.setGeometry(rgn)
                self.show()
    
            self.update()
        
        def paintEvent(self, ev):
            if self.dropArea is None:
                return
            p = QPainter(self)
            rgn = self.rect()
    
            p.setBrush(QBrush(QColor(100, 100, 255, 50)))
            p.setPen(QPen(QColor(50, 50, 150), 3))
            p.drawRect(rgn)
    
      # If anyone wants more extensive free help I run an online lab-like classroom-like 
      # message server feel free and drop by you will not be able to post until I clear 
      # you as a student as this prevents spammers so if interested here is the invite
      # https://discord.gg/3D8huKC
    

  • Banned

    Container.py contents

    from PyQt5.QtCore    import Qt, pyqtSignal
    from PyQt5.QtWidgets import QWidget, QGridLayout, QHBoxLayout, QStackedWidget, QSplitter
    
    import weakref
    
    from Dock import Dock as DockIt
    
    class Container(object):
        #sigStretchChanged = pyqtSignal()  ## can't do this here; not an QObject.
    
        def __init__(self, area):
            object.__init__(self)
    
            self.area = area
            self._container = None
            self._stretch = (10, 10)
            self.stretches = weakref.WeakKeyDictionary()
            
        def container(self):
            return self._container
            
        def containerChanged(self, c):
            self._container = c
    
        def type(self):
            return None
    
        def insert(self, new, pos=None, neighbor=None):
            if not isinstance(new, list):
                new = [new]
            if neighbor is None:
                if pos == 'before':
                    index = 0
                else:
                    index = self.count()
            else:
                index = self.indexOf(neighbor)
                if index == -1:
                    index = 0
                if pos == 'after':
                    index += 1
                    
            for n in new:
                #print "change container", n, " -> ", self
                n.containerChanged(self)
                #print "insert", n, " -> ", self, index
                self._insertItem(n, index)
                index += 1
                n.sigStretchChanged.connect(self.childStretchChanged)
            #print "child added", self
            self.updateStretch()
                
        def apoptose(self, propagate=True):
            ##if there is only one (or zero) item in this container, disappear.
            cont = self._container
            c = self.count()
            if c > 1:
                return
            if self.count() == 1:  ## if there is one item, give it to the parent container (unless this is the top)
                if self is self.area.topContainer:
                    return
                self.container().insert(self.widget(0), 'before', self)
            #print "apoptose:", self
            self.close()
            if propagate and cont is not None:
                cont.apoptose()
            
        def close(self):
            self.area = None
            self._container = None
            self.setParent(None)
            
        def childEvent(self, ev):
            ch = ev.child()
            if ev.removed() and hasattr(ch, 'sigStretchChanged'):
                #print "Child", ev.child(), "removed, updating", self
                try:
                    ch.sigStretchChanged.disconnect(self.childStretchChanged)
                except:
                    pass
                self.updateStretch()
            
        def childStretchChanged(self):
            #print "child", QObject.sender(self), "changed shape, updating", self
            self.updateStretch()
            
        def setStretch(self, x=None, y=None):
            #print "setStretch", self, x, y
            self._stretch = (x, y)
            self.sigStretchChanged.emit()
    
        def updateStretch(self):
            ###Set the stretch values for this container to reflect its contents
            pass
    
        def stretch(self):
            """Return the stretch factors for this container"""
            return self._stretch
    
    # Horizontal or Vertical Splitter with some changes:
    # - saveState/restoreState work correctly
    #
    class SplitContainer(Container, QSplitter):
        sigStretchChanged = pyqtSignal()
        
        def __init__(self, area, orientation):
            QSplitter.__init__(self)
            self.setOrientation(orientation)
            Container.__init__(self, area)
            #self.splitterMoved.connect(self.restretchChildren)
            
        def _insertItem(self, item, index):
            self.insertWidget(index, item)
            item.show()  ## need to show since it may have been previously hidden by tab
            
        def saveState(self):
            sizes = self.sizes()
            if all([x == 0 for x in sizes]):
                sizes = [10] * len(sizes)
            return {'sizes': sizes}
            
        def restoreState(self, state):
            sizes = state['sizes']
            self.setSizes(sizes)
            for i in range(len(sizes)):
                self.setStretchFactor(i, sizes[i])
    
        def childEvent(self, ev):
            QSplitter.childEvent(self, ev)
            Container.childEvent(self, ev)
    
        #def restretchChildren(self):
            #sizes = self.sizes()
            #tot = sum(sizes)
    
    class HContainer(SplitContainer):
        def __init__(self, area):
            SplitContainer.__init__(self, area, Qt.Horizontal)
            
        def type(self):
            return 'horizontal'
            
        def updateStretch(self):
            ##Set the stretch values for this container to reflect its contents
            #print "updateStretch", self
            x = 0
            y = 0
            sizes = []
            for i in range(self.count()):
                wx, wy = self.widget(i).stretch()
                x += wx
                y = max(y, wy)
                sizes.append(wx)
                #print "  child", self.widget(i), wx, wy
            self.setStretch(x, y)
            #print sizes
            
            tot = float(sum(sizes))
            if tot == 0:
                scale = 1.0
            else:
                scale = self.width() / tot
            self.setSizes([int(s*scale) for s in sizes])
    
    class VContainer(SplitContainer):
        def __init__(self, area):
            SplitContainer.__init__(self, area, Qt.Vertical)
            
        def type(self):
            return 'vertical'
    
        def updateStretch(self):
            ##Set the stretch values for this container to reflect its contents
            #print "updateStretch", self
            x = 0
            y = 0
            sizes = []
            for i in range(self.count()):
                wx, wy = self.widget(i).stretch()
                y += wy
                x = max(x, wx)
                sizes.append(wy)
                #print "  child", self.widget(i), wx, wy
            self.setStretch(x, y)
    
            #print sizes
            tot = float(sum(sizes))
            if tot == 0:
                scale = 1.0
            else:
                scale = self.height() / tot
            self.setSizes([int(s*scale) for s in sizes])
    
    class TContainer(Container, QWidget):
        sigStretchChanged = pyqtSignal()
    
        def __init__(self, area):
            QWidget.__init__(self)
            Container.__init__(self, area)
    
            self.layout = QGridLayout()
            self.layout.setSpacing(0)
            self.layout.setContentsMargins(0,0,0,0)
            self.setLayout(self.layout)
            
            self.hTabLayout = QHBoxLayout()
            self.hTabBox = QWidget()
            self.hTabBox.setLayout(self.hTabLayout)
            self.hTabLayout.setSpacing(2)
            self.hTabLayout.setContentsMargins(0,0,0,0)
            self.layout.addWidget(self.hTabBox, 0, 1)
    
            self.stack = QStackedWidget()
            self.layout.addWidget(self.stack, 1, 1)
            self.stack.childEvent = self.stackChildEvent
    
            self.setLayout(self.layout)
            for n in ['count', 'widget', 'indexOf']:
                setattr(self, n, getattr(self.stack, n))
    
        def _insertItem(self, item, index):
            if not isinstance(item, DockIt):
                raise Exception("Tab containers may hold only docks, not other containers.")
            self.stack.insertWidget(index, item)
            self.hTabLayout.insertWidget(index, item.label)
            #QObject.connect(item.label, SIGNAL('clicked'), self.tabClicked)
            item.label.sigClicked.connect(self.tabClicked)
            self.tabClicked(item.label)
            
        def tabClicked(self, tab, ev=None):
            if ev is None or ev.button() == Qt.LeftButton:
                for i in range(self.count()):
                    w = self.widget(i)
                    if w is tab.dock:
                        w.label.setDim(False)
                        self.stack.setCurrentIndex(i)
                    else:
                        w.label.setDim(True)
            
        def type(self):
            return 'tab'
    
        def saveState(self):
            return {'index': self.stack.currentIndex()}
            
        def restoreState(self, state):
            self.stack.setCurrentIndex(state['index'])
            
        def updateStretch(self):
            ##Set the stretch values for this container to reflect its contents
            x = 0
            y = 0
            for i in range(self.count()):
                wx, wy = self.widget(i).stretch()
                x = max(x, wx)
                y = max(y, wy)
            self.setStretch(x, y)
            
        def stackChildEvent(self, ev):
            QStackedWidget.childEvent(self.stack, ev)
            Container.childEvent(self, ev)
    
      # If anyone wants more extensive free help I run an online lab-like classroom-like 
      # message server feel free and drop by you will not be able to post until I clear 
      # you as a student as this prevents spammers so if interested here is the invite
      # https://discord.gg/3D8huKC
    


  • Hi Denni! I think you helped me out in my post on the python forums as well :) Thanks!

    Awesome, this is definitely very similar to what I ended up implementing (checking the mouse position against the child widget positions inside the dragMoveEvent method).

    What about the way I'm removing widgets from one layout and putting them in another? Is just setting the parent to None during the drag operation and then reparenting it okay? If they drop the widget onto nothing then it disappears and has no parent. This is the behavior I want, but is that sufficient for the widget to be deleted from memory?

    Thanks again.


  • Banned

    I am not sure if I understand what you are saying -- but if you drag/drop a QWidget outside a window it might become a stand alone window since every QWidget or anything for that matter based on a QtWidget is pretty much a window in its own right so not 100% if that would truly delete it. You will have to experiment with it to be sure.

    I think with this code that I shared if you drag/drop a widget outside the framework it becomes its own window... maybe been a while since I played with it.



  • Hm okay. At the moment it does disappear into the void but I haven't done testing to see if it's lingering around in memory anywhere. I'll experiment with it and see if I need to make a further call to delete it. Cheers.


  • Banned

    You could always use a I think self.close( ) and/or deleteLater( ) you might want to look into those -- I know I stumbled across an example where when you clicked a button it deleted the Widget it was contained within by using deleteLater( ) and I think self.close( ) but its been a while since I viewed that example


  • Banned

    @kainev you might want to look at this -- while I have not read it fully it does seem to address this issue
    https://stackoverflow.com/questions/20164015/is-deletelater-necessary-in-pyqt-pyside
    -- and this one adds to the above --
    https://forum.qt.io/topic/94369/when-exactly-does-qobject-deletelater-actually-delete/2



  • @Denni-0 say you have a window where you can drag and drop widgets into different areas, and it will reparent it to the new area's layout, if you drag one of the widgets off the window and 'drop' it anywhere that doesn't accept drops, it gets deleted instead.

    I'm not sure a call to close() would have an effect unless I set the WA_DeleteOnClose flag on all of my widgets that have the potential to be deleted, in which case I may as well just manually delete it in this particular instance with deleteLater.


  • Banned

    okay 1st I posted 2 different links the one that still exists and another one to a post on this forum which appears to have been deleted for some strange reason ??

    But I only stated self.close( ) and deleteLater( ) to be comprehensive in things you ought to look at and consider. Also be very, very careful that 2nd link that got deleted covered the issues of seemingly deleting a Widget when in fact it is not getting deleted and you may end up needing to destroy the widget instead but I would have to find that link again to point you to it but it is somewhere within this forum as @SGaist responded to it



  • I've gone with deleteLater. This seems to guarantee the behavior I'm after for my current usage:

        def mouseMoveEvent(self, event):
                mimeData = QMimeData()
                dragPixmap = self.grab()
        
                drag = QDrag(self)
                drag.setMimeData(mimeData)
                drag.setPixmap(dragPixmap)
                drag.setHotSpot(event.pos())
        
                self.setParent(None)
        
                drag.exec_(Qt.MoveAction)
    
                if not self.parent():
                    self.deleteLater()
    

    Using destroy seems to be rather dangerous, creating more potential problems than it solves. After reading both that thread you linked and QObject's documentation, I would myself be very hesitant to use destroy. Unless you found yourself in a situation where you were absolutely forced to I guess.

    Thanks again for your help :)


Log in to reply