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 spaceminimumHeight/minimumWidth: Sets minimum dimension constraintsmaximumHeight/maximumWidth: Sets maximum dimension constraintspreferredHeight/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:
- Mouse press records the starting position
- Mouse release calculates the distance from the start position
- If movement exceeds 10 pixels, the global screen coordinates are emitted via
dragDetectedsignal
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:
- The corresponding panel in the main window is hidden
- The floating window's position is set based on the drag release coordinates
- 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.