ImaginativeThinking.ca


A developers blog

Dynamic Menus: How to Populate a Menu from a Model in QtQuick

By: Brad

In this tutorial I’ll walk you through how to create a dynamic menu by populating it via a model.

A short while ago I ran into a situation where I needed to add a context menu to an application and to do this I choose to simply leverage the existing Qt Quick Control Menu so I wouldn’t have to deal with making sure the menu opened starting at the point where the user clicked the mouse nor have to deal with Z ordering. The one issue I ran into which made this not so cut and dry was that the menu items shown needed to be dynamic.

ThinkingPig

Depending on the users access level I was to expose different menu options; I could have defined the entire menu statically and binded the visibility property of each menu option to the users access level however some menu options were visible to lower leveled users if certain conditions were met and no user could see every single menu option. The logic to do all this was going to be verbose enough that I would probably end up putting it within a JavaScript function; That wouldn’t be so bad however the total number of menu options were going to be rather large (remember no user can see all the menu options so the total number of possible menu options were numerous) and statically defining them all wasn’t something I really wanted to do.

I thought to my self that this was a perfect candidate for a model where I controlled which menu options were visible via a proxy filter model. So I set out to do this.

First I noticed that the Qt Quick Control Menu has no model property so it does not support being populated via a model directly. No worries this is a Put-Stuff-Into-Other-Stuff-To-Make-New-Stuff type of GUI architecture all I needed to do was some how create N number of MenuItem elements within the Menu element in some sort of loop that is driven by a model…Repeater! Here was my first stab at it (MenuModel and UserMenuModel are C++ QStandardItemModel and QSortFilterProxyModel):

import QtQuick 2.2
import QtQuick.Controls 1.1
import ca.imaginativethinking.tutorial.menu 1.0

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("Dynamic Menu Sample 1")
 
   MenuModel {
        id: menuModel
    }
    UserMenuModel {
        id: userMenuModel
        model: menuModel
        userId: dynamicContextMenu.userId
    }
    Menu {
        id: dynamicContextMenu
        property alias userId: userMenuModel.userId

        Repeater {
            model: userMenuModel

            MenuItem {
                text: role_title
                onTriggered: {
                    menuModel.executeCommand( role_command, role_user );
                }
            }
        }
    }
}

My expectations here was that the Repeater would be driven by the UserMenuModel which is a QSortFilterProxyModel that is filtering the MenuModel so as to only show the menu options that this user was allowed to see. The Repeater would then create a MenuItem element for each visible menu option which in turn populates the Menu items property (which is the default property so anything in the body of Menu is automatically assigned to its property items) providing me with my nice little dynamic context menu (Put-Stuff-Into-Other-Stuff-To-Make-New-Stuff). Turns out however that does not work.

There is probably a few things wrong with my approach that wouldn’t have worked in the end but the big one is that Menu does not inherit from Item therefore its not so much Put-Stuff-Into-Other-Stuff-To-Make-New-Stuff after all, it has a few limitations which prevents my approach from working right out of the gate.

It took me a bit of spelunking through the internet to find a solution (and there is one) however upon sitting down to make this post my face turned red as the answer is also right in the Qt Documentation so lesson learned, always fully read the documentation :-). I choose to make the post anyways as I thought if I missed it so would others so this post is for you.

The Solution

So there is an element called Instantiator which is found within the QtQml module; it is used to dynamically create Qt Quick objects. The Instantiator element will create other Qt Quick objects, can be driven by a model, and manages the objects it creates. If the model that is driving the Instantiator changes the Instantiator will delete and create new objects as required. Sounds exactly like what I was looking for and the good news is that the Menu elements supports the user of Instantiator. In the end my approach was correct but I needed to use an Instantiator and not a Repeater.

import QtQuick 2.2
import QtQuick.Controls 1.1
import ca.imaginativethinking.tutorial.menu 1.0

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("Dynamic Menu Sample 1")
 
   MenuModel {
        id: menuModel
    }
    UserMenuModel {
        id: userMenuModel
        model: menuModel
        userId: dynamicContextMenu.userId
    }
    Menu {
        id: dynamicContextMenu
        property alias userId: userMenuModel.userId

        Instantiator {
            model: userMenuModel
            onObjectAdded: dynamicContextMenu.insertItem( index, object )
            onObjectRemoved: dynamicContextMenu.removeItem( object )
            delegate: MenuItem {
                text: role_title
                onTriggered: {
                    menuModel.executeCommand( role_command, role_user );
                }
            }
        }
    }
}

So to create a dynamic menu using the Qt Quick Control Menu we need to create an instance of an Instantiator and provide it with a delegate (note the delegate is the default property so we could omit the ‘delegate:’ part, any items within the Instantiator body are by default added to the delegate property) of a MenuItem. Here we are telling the Instantiator for each entry in the model create a new MenuItem instance per the provided template (delegate). At this point though the newly created MenuItem objects belong to the Instantiator and not to the Menu.

Remember objects created by the Instantiator belong to the Instantiator, it is responsible for managing them. So in order to include the newly created MenuItem‘s to (and remove the deleted ones from) the Menu we need to provide some implementation to the Instantiator‘s onObjectAdded() and onObjectRemoved() slots. These slots get executed each time a new item is added and/or removed from the Instantiator. You can see here that when we change the filter on the model and cause new entries to be added the Instantiator will create a new MenuItem instance for the new entry then execute the onObjectAdded() slot. Our implementation of that slot adds the MenuItem to the Menu object and tada we have a dynamically populated context menu.

So there you go that is all you have to do to create a dynamic menu which is driven by a model in Qt Quick.

You can download a sample application which illustrate the above here:
Qt Quick Dynamic Menu Sample 1

Thank you I hope you have enjoyed and found this tutorial helpful. Feel free to leave any comments or questions you might have below and I’ll try to answer them as time permits.

Until next time think imaginatively and design creatively

Brad

My interest in computer programming started back in high school and Software Development has remained a hobby of mine ever since. I graduated as a Computer Engineering Technologist and have been working as a Software Developer for many years. I believe that software is crafted; understanding that how it is done is as important as getting it done. I enjoy the aesthetics in crafting elegant solutions to complex problems and revel in the knowledge that my code is maintainable and thus, will have longevity. I hold the designation Certified Technician (C.Tech.) with the Ontario Association of Computer Engineering Technicians and Technologists (OACETT), have been certified as a Professional Scrum Master level 1 (PSM I) and as a Professional Scrum Developer level 1 (PSD I) by Scrum.org as well as designated as an Officially Certified Qt Developer by the Qt Company. For more on my story check out the about page here

Feel free to write a reply or comment.