Building a Resizable and Detachable Panel UI Framework with QML

Overview

This article demonstrates how to create a QML-based UI framework with resizable panels that can be dragged out as independant floating windows. The implementation uses QtQuick.Controls 2.14 and addresses two core requirements: flexible panel sizing and window detachment functionality.

Requirements:

  • Modular interface with adjustable panel dimensions
  • Ability to drag panels out as standalone windows

Implementation

Panel Resizing with SplitView

The SplitView component from QtQuick.Controls provides built-in resizable panel functionality, similar to Qt's QSplitter. We can combine horizontal and vertical split views to create a flexible layout.

Here's the main layout structure:

SplitView {
    id: rootSplitView
    anchors.fill: parent
    anchors.margins: 4
    orientation: Qt.Vertical
    handle: dividerComponent

    SplitView {
        SplitView.fillHeight: true
        SplitView.minimumHeight: 200
        orientation: Qt.Horizontal
        handle: dividerComponent

        MediaBrowserPanel {
            id: mediaPanel
            SplitView.preferredWidth: 200
            SplitView.maximumWidth: 250
            SplitView.minimumWidth: 100
        }
        VideoPlayerPanel {
            id: playerPanel
            SplitView.fillWidth: true
            SplitView.minimumWidth: 100
        }
        SettingsPanel {
            id: settingsPanel
            SplitView.preferredWidth: 200
            SplitView.fillWidth: true
            SplitView.minimumWidth: 100
        }
    }

    TimelinePanel {
        id: timelinePanel
        SplitView.preferredHeight: 160
        SplitView.minimumHeight: 100
    }
}

Component {
    id: dividerComponent
    Rectangle {
        implicitWidth: 4
        implicitHeight: 4
        opacity: 0
    }
}

Layout properties explained:

  • fillHeight / fillWidth: Automatically fills available space
  • minimumHeight / minimumWidth: Sets minimum dimension constraints
  • maximumHeight / maximumWidth: Sets maximum dimension constraints
  • preferredHeight / preferredWidth: Sets default dimenison values

The divider handle is hidden by setting opacity: 0 on a transparent Rectangle.

Drag Detection for Window Detachment

Each detachable panel needs a common base component that handles drag interactions. When the user drags the panel beyond a threshold distance, a signal is emitted to trigger window detachment.

Item {
    property color panelBackground: "#1b1b1b"
    property string panelTitle: "Panel"
    signal dragDetected(var globalPosition)

    Rectangle {
        anchors.fill: parent
        color: panelBackground
        radius: 8
        Text {
            color: "#ffffffff"
            text: panelTitle
            font.bold: true
            anchors.centerIn: parent
        }
    }

    Rectangle {
        width: parent.width
        height: headerBar.radius
        anchors.bottom: headerBar.bottom
        color: "#2d2d2d"
    }

    Rectangle {
        id: headerBar
        width: parent.width
        height: 30
        color: "#2d2d2d"
        radius: 8

        MouseArea {
            id: dragRegion
            anchors.fill: parent
            property int startX: -1
            property int startY: -1

            onPressed: function(mouse) {
                console.log("Drag started at:", mouse.x, mouse.y)
                startX = mouse.x
                startY = mouse.y
            }

            onReleased: function(mouse) {
                console.log("Drag ended at:", mouse.x, mouse.y)
                if (startX >= 0 && startY >= 0) {
                    let deltaX = mouse.x - startX
                    let deltaY = mouse.y - startY
                    let movement = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
                    if (movement > 10) {
                        dragDetected(mapToGlobal(mouse.x, mouse.y))
                    }
                }
                startX = -1
                startY = -1
            }
        }
    }
}

The drag logic works as follows:

  1. Mouse press records the starting position
  2. Mouse release calculates the distance from the start position
  3. If movement exceeds 10 pixels, the global screen coordinates are emitted via dragDetected signal

Floating Window Managemant

The main window contains a hidden floating window that displays detached panels. When a panel is dragged out, the floating window becomes visible and loads the corresponding QML component.

import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14

Window {
    visible: true
    width: 800
    height: 480
    title: qsTr("Detachable Panels Demo")

    // Main layout with panels
    SplitView {
        id: mainLayout
        anchors.fill: parent
        anchors.margins: 4
        orientation: Qt.Vertical
        handle: hiddenDivider

        SplitView {
            SplitView.fillHeight: true
            SplitView.minimumHeight: 200
            orientation: Qt.Horizontal
            handle: hiddenDivider

            MediaBrowserPanel {
                id: mediaPanel
                SplitView.preferredWidth: 200
                SplitView.maximumWidth: 250
                SplitView.minimumWidth: 100
            }
            VideoPlayerPanel {
                id: playerPanel
                SplitView.fillWidth: true
                SplitView.minimumWidth: 100
            }
            SettingsPanel {
                id: settingsPanel
                SplitView.preferredWidth: 200
                SplitView.fillWidth: true
                SplitView.minimumWidth: 100
            }
        }

        TimelinePanel {
            id: timelinePanel
            SplitView.preferredHeight: 160
            SplitView.minimumHeight: 100
        }
    }

    // Hidden floating window for detached panels
    Window {
        id: floatingPanel
        visible: false
        width: 330
        height: 380
        property string componentSource: ""
        property var contentLoader: componentLoader

        Loader {
            id: componentLoader
            anchors.fill: parent
        }
    }

    // Handle floating window visibility changes
    Connections {
        target: floatingPanel
        onVisibleChanged: {
            if (floatingPanel.visible) {
                if (floatingPanel.componentSource !== "") {
                    floatingPanel.contentLoader.source = floatingPanel.componentSource
                }
            } else {
                // Restore all panels in main window
                mediaPanel.visible = true
                playerPanel.visible = true
                settingsPanel.visible = true
                timelinePanel.visible = true
            }
        }
    }

    // Connect drag signals from each panel
    Connections {
        target: mediaPanel
        onDragDetected: function(position) {
            showFloatingWindow(0, position)
        }
    }

    Connections {
        target: playerPanel
        onDragDetected: function(position) {
            showFloatingWindow(1, position)
        }
    }

    Connections {
        target: settingsPanel
        onDragDetected: function(position) {
            showFloatingWindow(2, position)
        }
    }

    Connections {
        target: timelinePanel
        onDragDetected: function(position) {
            showFloatingWindow(3, position)
        }
    }

    function showFloatingWindow(panelType, position) {
        if (floatingPanel.visible) {
            return false
        }

        let qmlFile
        switch (panelType) {
            case 0:
                qmlFile = "MediaBrowserPanel.qml"
                mediaPanel.visible = false
                break
            case 1:
                qmlFile = "VideoPlayerPanel.qml"
                playerPanel.visible = false
                break
            case 2:
                qmlFile = "SettingsPanel.qml"
                settingsPanel.visible = false
                break
            case 3:
                qmlFile = "TimelinePanel.qml"
                timelinePanel.visible = false
                break
        }

        floatingPanel.componentSource = qmlFile

        if (position) {
            floatingPanel.x = position.x - floatingPanel.width / 2
            floatingPanel.y = position.y
        }

        floatingPanel.show()
        return true
    }
}

The Loader component dynamically loads QML files based on the source property. When a panel is detached:

  1. The corresponding panel in the main window is hidden
  2. The floating window's position is set based on the drag release coordinates
  3. The appropriate QML file is loaded into the floating window

Summary

This framework provides a complete solution for resizable and detachable QML panels. The SplitView component handles layout resizing, while MouseArea and custom signals enable drag-to-detach functionality. The Loader component manages dynamic content loading for floating windows.

Tags: QML Qt qtquick qtquick-controls ui-framework

Posted on Mon, 06 Jul 2026 16:59:51 +0000 by tnkannan