Skip to content

Instantly share code, notes, and snippets.

@alex-spataru
Last active August 8, 2024 02:04
Show Gist options
  • Save alex-spataru/ee5e74f82a72a0a2e446766a77c43665 to your computer and use it in GitHub Desktop.
Save alex-spataru/ee5e74f82a72a0a2e446766a77c43665 to your computer and use it in GitHub Desktop.
Implementation of a simple and clean Material drawer for your QtQuick/QML applications

Description

This gist allows you to implement a material drawer easily for your projects. It consists of three files:

  • PageDrawer.qml The drawer itself, with an icon viewer, a list view and some hacks to execute the actions/functions assigned to each drawer item
  • DrawerItem.qml Which can act as an action, a spacer, a separator or a link
  • SvgImage.qml Ugly hack to make SVG images look crisp and smooth on HDPI screens

Licence

This code is released under the WTFPL, for more information click here.

Example project screenshot

Screenshot

Requirements

This Gist requires you to use QtQuick.Controls 2.0 and QtQuick.Layouts 1.0, however, it could be adapted to work on previous versions of QtQuick.

Usage

import QtQuick 2.0

PageDrawer {
    id: drawer

    //
    // Icon properties
    //
    iconTitle: "Test App"
    iconSource: "qrc:/images/logo.png"
    iconSubtitle: qsTr ("Version 1.0 Beta")

    //
    // Define the actions to take for each drawer item
    // Drawers 5 and 6 are ignored, because they are used for
    // displaying a spacer and a separator
    //
    actions: {
        0: function() { console.log ("Item 1 clicked!") },
        1: function() { console.log ("Item 2 clicked!") },
        2: function() { console.log ("Item 3 clicked!") },
        3: function() { console.log ("Item 4 clicked!") },
        4: function() { console.log ("Item 5 clicked!") },
        7: function() { console.log ("Item 6 clicked!") },
        8: function() { console.log ("Item 7 clicked!") }
    }

    //
    // Define the drawer items
    //
    items: ListModel {
        id: pagesModel

        ListElement {
            pageTitle: qsTr ("Item 1")
            pageIcon: "qrc:/icons/item1.svg"
        }

        ListElement {
            pageTitle: qsTr ("Item 2")
            pageIcon: "qrc:/icons/item2.svg"
        }

        ListElement {
            pageTitle: qsTr ("Item 3")
            pageIcon: "qrc:/icons/item3.svg"
        }

        ListElement {
            pageTitle: qsTr ("Item 4")
            pageIcon: "qrc:/icons/item4.svg"
        }

        ListElement {
            pageTitle: qsTr ("Item 5")
            pageIcon: "qrc:/icons/item5.svg"
        }

        ListElement {
            spacer: true
        }

        ListElement {
            separator: true
        }

        ListElement {
            pageTitle: qsTr ("Item 6")
            pageIcon: "qrc:/icons/item6.svg"
        }

        ListElement {
            pageTitle: qsTr ("Item 7")
            pageIcon: "qrc:/icons/item7.svg"
        }
    }
}
import QtQuick 2.0
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.0
ItemDelegate {
//
// Do not allow user to click spacers and separators
//
enabled: !isSpacer (index) && !isSeparator (index)
//
// Alias to parent list view
//
property ListModel model
property ListView pageSelector
//
// Returns true if \c spacer is defined and is equal to \c true
//
function isSpacer (index) {
if (typeof (model.get (index).spacer) !== "undefined")
return model.get (index).spacer
return false
}
//
// Returns true if \c link is defined and is equal to \c true
//
function isLink (index) {
if (typeof (model.get (index).link) !== "undefined")
return model.get (index).link
return false
}
//
// Returns true if \c separator is defiend and is equal to \c true
//
function isSeparator (index) {
if (typeof (model.get (index).separator) !== "undefined")
return model.get (index).separator
return false
}
//
// Returns the icon for the drawer item
//
function iconSource (index) {
if (typeof (model.get (index).pageIcon) !== "undefined")
return model.get (index).pageIcon
return ""
}
//
// Returns the title for the drawer item
//
function itemText (index) {
if (typeof (model.get (index).pageTitle) !== "undefined")
return model.get (index).pageTitle
return ""
}
//
// Returns \c true if separatoText is correctly defined
//
function hasSeparatorText (index) {
return isSeparator (index) && typeof (model.get (index).separatorText) !== "undefined"
}
//
// Decide if we should highlight the item
//
highlighted: ListView.isCurrentItem ? !isLink (index) : false
//
// Calculate height depending on the type of item that we are
//
height: {
if (isSpacer (index)) {
var usedHeight = 0
for (var i = 0; i < model.count; ++i) {
if (!isSpacer (i)) {
if (!isSeparator (i) || hasSeparatorText (i))
usedHeight += 48
else
usedHeight += 8
}
}
return Math.max (8, pageSelector.height - usedHeight)
}
if (enabled || hasSeparatorText (index))
return 48
return 8
}
//
// Separator layout
//
ColumnLayout {
spacing: 8
anchors.fill: parent
visible: isSeparator (index)
anchors.verticalCenter: parent.verticalCenter
Item {
Layout.fillHeight: true
}
Rectangle {
height: 0.5
opacity: 0.20
color: "#000000"
anchors {
left: parent.left
right: parent.right
}
}
Label {
opacity: 0.54
color: "#000000"
font.pixelSize: 14
font.weight: Font.Medium
text: hasSeparatorText (index) ? separatorText : ""
anchors {
margins: 16
left: parent.left
right: parent.right
}
}
Item {
Layout.fillHeight: true
}
}
//
// Normal layout
//
RowLayout {
spacing: 16
anchors.margins: 16
anchors.fill: parent
visible: !isSpacer (index)
Image {
smooth: true
opacity: 0.54
fillMode: Image.Pad
source: iconSource (index)
sourceSize: Qt.size (24, 24)
verticalAlignment: Image.AlignVCenter
horizontalAlignment: Image.AlignHCenter
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: 36 - (2 * spacing)
}
Label {
opacity: 0.87
font.pixelSize: 14
text: itemText (index)
Layout.fillWidth: true
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
import QtQuick 2.0
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.0
import QtGraphicalEffects 1.0
Drawer {
id: drawer
//
// Default size options
//
implicitHeight: parent.height
implicitWidth: Math.min (parent.width > parent.height ? 320 : 280,
Math.min (parent.width, parent.height) * 0.90)
//
// Icon properties
//
property string iconTitle: ""
property string iconSource: ""
property string iconSubtitle: ""
property size iconSize: Qt.size (72, 72)
property color iconBgColorLeft: "#de6262"
property color iconBgColorRight: "#ffb850"
//
// List model that generates the page selector
// Options for selector items are:
// - spacer: acts an expanding spacer between to items
// - pageTitle: the text to display
// - separator: if the element shall be a separator item
// - separatorText: optional text for the separator item
// - pageIcon: the source of the image to display next to the title
//
property alias items: listView.model
property alias index: listView.currentIndex
//
// Execute appropiate action when the index changes
//
onIndexChanged: {
var isSpacer = false
var isSeparator = false
var item = items.get (index)
if (typeof (item) !== "undefined") {
if (typeof (item.spacer) !== "undefined")
isSpacer = item.spacer
if (typeof (item.separator) !== "undefined")
isSpacer = item.separator
if (!isSpacer && !isSeparator)
actions [index]()
}
}
//
// A list with functions that correspond with the index of each drawer item
// provided with the \a pages property
//
// For a string-based example, check this SO answer:
// https://stackoverflow.com/a/26731377
//
// The only difference is that we are working with the index of each element
// in the list view, for example, if you want to define the function to call
// when the first item of the drawer is clicked, you should write:
//
// actions: {
// 0: function() {
// console.log ("First item clicked!")
// },
//
// 1: function() {}...,
// 2: function() {}...,
// n: function() {}...
// }
//
property var actions
//
// Main layout of the drawer
//
ColumnLayout {
spacing: 0
anchors.margins: 0
anchors.fill: parent
//
// Icon controls
//
Rectangle {
z: 1
height: 120
id: iconRect
Layout.fillWidth: true
Rectangle {
anchors.fill: parent
LinearGradient {
anchors.fill: parent
start: Qt.point (0, 0)
end: Qt.point (parent.width, 0)
gradient: Gradient {
GradientStop { position: 0; color: iconBgColorLeft }
GradientStop { position: 1; color: iconBgColorRight }
}
}
}
RowLayout {
spacing: 16
anchors {
fill: parent
centerIn: parent
margins: 16
}
Image {
source: iconSource
sourceSize: iconSize
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.fillHeight: true
Item {
Layout.fillHeight: true
}
Label {
color: "#fff"
text: iconTitle
font.weight: Font.Medium
font.pixelSize: 16
}
Label {
color: "#fff"
opacity: 0.87
text: iconSubtitle
font.pixelSize: 12
}
Item {
Layout.fillHeight: true
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
//
// Page selector
//
ListView {
z: 0
id: listView
currentIndex: -1
Layout.fillWidth: true
Layout.fillHeight: true
Component.onCompleted: currentIndex = 0
delegate: DrawerItem {
model: items
width: parent.width
pageSelector: listView
onClicked: {
if (listView.currentIndex !== index)
listView.currentIndex = index
drawer.close()
}
}
ScrollIndicator.vertical: ScrollIndicator { }
}
}
}
import QtQuick 2.0
//
// Used to avoid showing blurry SVG images on hDPI screens
// Taken from: https://stackoverflow.com/a/38636816
//
Item {
property alias image: img
property alias source: img.source
property alias fillMode: img.fillMode
property alias sourceSize: img.sourceSize
property alias verticalAlignment: img.verticalAlignment
property alias horizontalAlignment: img.horizontalAlignment
implicitWidth: sourceSize.width
implicitHeight: sourceSize.height
Image {
id: img
anchors.centerIn: parent
sourceSize.width: width * DevicePixelRatio
sourceSize.height: height * DevicePixelRatio
}
}
@alex-spataru
Copy link
Author

Under which license is your code? BSD?

Hi, feel free to do anything with this code. I uploaded this gist so that I don't have to check my previous projects to make a drawer. Consider it to be released under the WTFPL. 🖖

@danieloneill
Copy link

Awesome work, I like it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment