Menus and Widgets in Qt

published at 06.08.2015 12:15 by Jens Weller
Save to Instapaper Pocket

The fourth part of this series on developing applications in C++ with Qt and boost is about handling menus and getting a first view at widgets. Lets shortly sum up the current status of the application: the tree inside the tree view contains all data, and the name of these nodes is displayed in the tree. The last episode was about writing a factory using boost::factory, so that a corresponding factory can be invoked for a type. The use case is, to create the form like window that allows to edit this instance. Now, I want to take a closer look, how to display menus in Qt, with a context menu in the tree view, it would be possible to allow interaction with a single node, without needing an actual form to invoke actions.

The actions I want to offer in the tree view via a context menu are quite simple: create new Dir or Page items, and the ability to delete an item. This is by the way the only flaw, which my tree class and model had, it had no way to delete items...

Menus

Lets start with how Qt sees and handles menus. You can easily create a window menu in the RAD Editor of QtCreator, and then add a slot for its triggered() signal. Window menus created in the RAD Editor can easily be connected to slots using connect:

connect(ui->actionQuit,SIGNAL(triggered()),this,SLOT(close()));
connect(ui->actionNew_Document,SIGNAL(triggered()),this,SLOT(createDocument()));

You can continue reading, but I came up today with a much nicer, cleaner and generic solution: A generic context menu class for Qt.

But for a context menu, it does not make sense to go this way. For Qt, every menu is a collection of QAction items, so that a QList<QAction*> is the base of our context menu. I really like to use lambdas when I have to setup such code, which has to create certain elements, and still call some methods to get the "correct" version of the object. This is how I currently initialize the different QList objects for the context menus:

auto setup_action = [](QList<QAction*>& actions,const QString& text,const QVariant& v,QObject* parent)
{
    actions.push_back(new QAction(text,parent));
    actions.last()->setData(v);
};
setup_action(type2menu[dir_typeid],"new Page",NEW_PAGE,this);
setup_action(type2menu[dir_typeid],"new Dir",NEW_DIR,this);
setup_action(type2menu[dir_typeid],"delete Item",DELETE,this);
setup_action(type2menu[page_typeid],"delete Item",DELETE,this);
setup_action(type2menu[document_typeid],"close Document",DELETE,this);

The lambda takes 4 arguments: the QList it self, the name of the menu item to add, the corresponding ID for what this menu item should do, which is stored in the QVariant data property of QAction, and the usual parent QObject pointer so often used in Qt. This needs to be stored in some way, that a type can have its own menu, so I have a flat_map<size_t, QList<QAction*> >. This code abuses the index operator to force to create the list in the first call.

Now, the context menu shall be displayed when a right mouse click is made on the tree view. Handling mouse clicks in Qt is not always the easiest thing, as many controls do not offer signals for this, in this case one either has to derive a new class from the control, and override some virtual methods or install an event filter. I first went for the second approach, but then saw that there are the signals I need in a QTreeView. So I connect a SLOT to the corresponding signal:

connect(ui->treeView,SIGNAL(customContextMenuRequested(QPoint)),this,SLOT(showContextMenu(QPoint)));

In order to get this working, you still have to change the contextMenuPolicy to "CustomMenuPolicy", only then your slot will be called when a context menu is requested. This is the code handling the display of the menu:

void MainWindow::showContextMenu(QPoint pos)
{
    QModelIndex index =ui->treeView->indexAt(pos);
    if(!index.isValid())return;

    auto item = static_cast< ItemTreeModel::ItemPtr >(index.internalPointer());
    if(type2menu.find(item->type_id())== type2menu.end())//some items have no submenu...
        return;
    auto action = QMenu::exec(type2menu[item->type_id()],mapToGlobal(pos));
    if(action)
    {
        switch(action->data().toInt())
        {
        case NEW_DIR:
            createInstance< Dir >(index,"Enter Directory Name:");
            break;
... default: qDebug() << "invalid menu id!"; } } }

The slot has only the position as an argument, so the first thing to do is to obtain the QModelIndex to which the click is corresponding. If that is valid, the already known ItemPtr is extracted, then the actual Menu code follows. The static method QMenu::exec displays the menu, it needs the QList<QAction*> plus the position, which has to be translated to global coordinates in the window. QMenu::exec returns a QAction pointer, which is the clicked item, or a nullptr if no item was clicked.

Each QAction has an enum variable as its data, which is then used in the switch. The createInstance method from last episode is called for the case "new Dir" was clicked. The case to delete an item is a bit more tricky then a one liner:

case DELETE:
{
    auto pwidget = factory.removeWidget(item->id(),item->type_id());
    if(pwidget)
    {
        int tabindex = ui->tabWidget->indexOf(pwidget);
        if(tabindex != -1)
            ui->tabWidget->removeTab(tabindex);
        pwidget->deleteLater();
    }
    treemodel->erase(index);
}

The corresponding widget needs to be erased from the cache in the factory, and removed from the tab control, but only if it exists in the first place. The tabs are also closeable, which is achieved with setting the property tabsCloseable to true (check box in the property editor), and then the signal needs to be connected, this time I'm using the new connect syntax which allows for using lambdas as slots in Qt:

connect(ui->tabWidget,&QTabWidget::tabCloseRequested,[this](int index){ui->tabWidget->removeTab(index);});

Widgets

The last episode was about building a factory to produce widgets, when an item is double clicked. These widgets are meant to display the data of the clicked item. As an example, the DirPanel class:

class DirPanel : public QWidget
{
    Q_OBJECT
    ItemTreeModel::SharedItem item;
    Dir* dir = nullptr;
public:
    explicit DirPanel(const std::function< void (const ItemTreeModel::SharedItem &, QWidget *)>& updateItem,const ItemTreeModel::SharedItem &item, QWidget *parent = 0);
    ~DirPanel();
private:
    Ui::DirPanel *ui;
};

I have the habit to call these classes panels, this reaches back to when I worked with wxWidgets, and such classes where derived from wxPanel instead of QWidget. Each class holds a shared_ptr to the item, and a pointer to the actual data class, as this is only stored as a variant inside the tree item class. All constructors have these 3 parameters, where the first is a callback to the mainwindow, the 3rd the QWidget* parent, taking ownership of this instance.

The callback to the mainwindow class notifies the tree view of that a name property in the tree has changed and needs to be updated:

void MainWindow::updateItem(const ItemTreeModel::SharedItem &item, QWidget* source)
{
    if(source)
        ui->tabWidget->setTabText(ui->tabWidget->indexOf(source),QString::fromStdString(item->name()));
    treemodel->notifyDataChanged(item.get());
}

When the name of an item is changed, it needs to change in the tab control, but also in the tree. The notifyDataChanged method simply constructs a QModelIndex and emits the dataChanged signal:

void ItemTreeModel::notifyDataChanged(ItemPtr item)
{
    QModelIndex topLeft = createIndex(item->row(),0,(void*)item);
    emit dataChanged(topLeft,topLeft);
}

At the current point, the program can display data, notify the main window about changes, but there is more to widgets in the next episode...

Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!