Using QWidgetActions to place arbitrary widgets inside command-menus.

Again, on the subject of finding unusual ways to use the Qt GUI Library, to solve certain atypical problems that can exist in the design of applications. What a programmer might want to do, beyond giving his application command-menus, that fire Signals, which are received by Slots, which in turn evoke the capabilities of his or her program. The programmer may feel that he needs an entry in this command-menu to have an unusual appearance, such as, to consist of blue text, with a background that goes from being neutral to being yellow, when this entry is hovered over with the mouse…

Well, Qt has a special base-class for that sort of thing, which is called ‘QWidgetAction’. The idea behind it is, that it inherits the ‘QAction’ class, that would normally be added to the menus, but in such a way, that it additionally connects with a ‘QLabel’ object, which in turn can be given the most striking appearances, including rich text in the form of HTML.

This is what the result looks like:

Screenshot_20200821_021223_e

The ‘Reset Program’ command is different in appearance, from the other menu entries.

This is the code that was required, to make it happen:

The file ‘menubar.h‘ –

 


#ifndef MENUBAR_H
#define MENUBAR_H

#include "mainwindow.h"

class SimpleMenu : public QWidget
{

public:
    SimpleMenu(MainWindow *parent = 0);

};

class HoverWidget : public QLabel
{
public:
    HoverWidget(QWidget *parent);

    void enterEvent(QEvent * event);
    void leaveEvent(QEvent * event);
};

#endif // MENUBAR_H


 

The file ‘menubar.cpp‘ –

 


#include "menubar.h"
#include "mainwindow.h"
#include <QApplication>
#include <QMenu>
#include <QMenuBar>
#include <QWidgetAction>

SimpleMenu::SimpleMenu(MainWindow *parent)
    : QWidget(parent)
{

    QAction *quit = new QAction("&Quit", this);
    quit->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q));

//    QAction *reset_window = new QAction(this);
//    reset_window->setText("Re&set Program!");
//    reset_window->setShortcut(Qt::CTRL + Qt::Key_0);

    QAction *zoom_in = new QAction("Zoom I&n", this);
    QAction *zoom_out = new QAction("Zoom Ou&t", this);

    zoom_in->setShortcut(Qt::CTRL + Qt::Key_Plus);
    zoom_out->setShortcut(Qt::CTRL + Qt::Key_Minus);

    QMenuBar *toplev = new QMenuBar(parent);

    QMenu *file;
    file = toplev->addMenu("&File");
    file->addAction(zoom_in);
    file->addAction(zoom_out);
    file->addSeparator();
    file->addAction(quit);

    QMenu *help;
    help = toplev->addMenu("&Help");

    HoverWidget *reset_window_label = new HoverWidget(help);
    reset_window_label->setText("Reset Program            Ctrl+0");
    reset_window_label->setStyleSheet("QLabel {color: blue; margin: 4px}");
    QWidgetAction *reset_window = new QWidgetAction(this);
    reset_window->setDefaultWidget(reset_window_label);
    reset_window->setShortcut(Qt::CTRL + Qt::Key_0);

    help->addAction(reset_window);

    //  Qt5 Semantics for Signals sent to Slots...

    connect(quit, &QAction::triggered, qApp, &QApplication::quit);

    connect(reset_window, &QAction::triggered, parent, &MainWindow::resetQ);
    connect(zoom_in, &QAction::triggered, parent, &MainWindow::zoom_in_do);
    connect(zoom_out, &QAction::triggered, parent, &MainWindow::zoom_out_do);

}

HoverWidget::HoverWidget(QWidget *parent) :
    QLabel(parent)
{
    setAttribute(Qt::WA_Hover, true);
}

void HoverWidget::enterEvent(QEvent *event)
{
    setStyleSheet("QLabel {color: blue; background-color: yellow; margin: 4px}");
    QLabel::enterEvent(event);
}

void HoverWidget::leaveEvent(QEvent *event)
{
    setStyleSheet("QLabel {color: blue; margin: 4px}");
    QLabel::leaveEvent(event);
}

 

The exercise in which I tested this feature can be found at the following URL within my own site:

https://dirkmittler.homeip.net/binaries/

And, the relevant compressed archives are named ‘Creator_Test3.tar.gz‘ and ‘Creator_Test3.zip‘.

(This is a link to the previous exercise.)

 

(Update 8/22/2020, 17h40: )

 

(Previous Update, 8/21/2020, 11h55: )

There is a slight misbehaviour that remains, when the task has effectively been accomplished in this way. While the ‘QWidgetAction’ object is being displayed, as part of its parent ‘QMenu’ object, the keyboard shortcut doesn’t work, which in my example was ‘Ctrl+0‘, to accomplish what clicking on this widget accomplishes. The user needs to dismiss the menu, before keyboard shortcut equivalents work again.

The reason this happens is due to logic. My ‘HoverWidget’ class sub-classes ‘QLabel’, and not, ‘QWidgetAction’. The reason I made this decision was, the fact that mouse-hover events are supposed to switch-in a different style-sheet for the label, and not, to have any effect on the ‘QWidgetAction’ object. And apparently, when displayed, my ‘HoverWidget’ object receives other events, not just mouse-hover events.

I did what I was supposed to do, which was, to pass the events to the corresponding methods of the parent-class, after my overridden methods handled them, in case there was default handling of any events that needed to take place. But that parent class is ‘QLabel’ and not ‘QWidgetAction’ ! 8-O  It would be the ‘QWidgetAction’ object, that has this keyboard shortcut defined, and that would respond to it.

And so, while this widget is being shown, the user of my sample application will either need to click on it, or dismiss it, before he can use keyboard shortcuts again. (:1)


 

 

(Update 8/21/2020, 12h35: )

There is another caveat to using the ‘QWidgetAction’ class. Its style will never match the style of the rest of the menu perfectly, no matter how much the programmer messes with the style-sheet of the associated label.

The way I discovered this, had to do with what my motivation was to start using ‘QWidgetAction’, aside from the fact that my larger goal is, to teach myself how to use Qt5, and, to pass my knowledge along to the public.

On the Plasma 5.8 desktop manager of the Computer ‘Phosphene’, which is a Debian 9 / Stretch -based computer, and the computer I am running my exercises on, apparently, the designers of the desktop theme (“Breeze”) decided that it would be a good feature, that the desktop manager should find ‘Mnemonics’ for every Menu-Entry, for every application that has a Menu-Bar. Normally, under Qt, the way the programmer decides that some menu-actions are supposed to have these mnemonics, which are shown as underlined single letters, is to put an ampersand in front of the chosen letter. The way a literal ampersand gets put in the text, is by the programmer putting a pair of ampersands, in the text which he or she assigns to the ‘QAction’ object.

The mnemonics allow the user to navigate the menus, just by typing letters on his keyboard.

The point is that, if the programmer did not designate a mnemonic, there should not be one. Sometimes it’s nice to have a menu-entry, which has no mnemonics. But as I just wrote, my desktop manager not only requires me to hold down the left <Alt> key to see those mnemonics, but actually adds them to menu-actions which the programmer did not wish to have any!

In the case of the ‘Reset Program’ menu-item, my search for a way to bypass this led to the use of a ‘QWidgetAction’ object.

Because the style-sheets of their labels will never match the text of the menus they are contained in, I think that the best thing to do is, to make them deliberately different in appearance, in some obvious way. In my case, I still needed to specify a ‘margin’ in my style-sheet, since, to have the text boxed tightly will just look ugly. But I also needed to make the text blue. That way, the difference in style is made stronger, the user can see that the object is of a different type, and then, the user does not see an ugly menu, one entry of which just seems defective. An entry which is just slightly misaligned, or which seems to be in the wrong font, ‘looks defective’, while an entry that is in a different colour, with a background that animates, just ‘looks different’.

 


 

(Update 8/21/2020, 15h45: )

I have inquired further, about the subject that, when the ‘QWidgetAction’ object is being displayed, its shortcut, which works when the object is not being displayed, does not seem to work. As it turns out, this is a feature, and is not an impediment to implementing this (differently behaving) menu entry. The reason for this is the fact that by default, the object will or won’t have focus as it should, and when it does, simply typing <Enter> will trigger it. However, there would still be an issue with the code I posted here…

As I left it, there is no way for the user to see, whether a given ‘QWidgetAction’ has focus or not, because my code only changes the appearance (of the associated, ‘QLabel’ -derived object), when the mouse hovers or does not. If there were several ‘QWidgetAction’ entries in one menu, then the user would want to know, which of those he’d be triggering, when he types <Enter>.

Now, the code that highlights focus-in or focus-out, would be relatively straightforward. The existing ‘HoverWidget’ sub-class would need to override the following methods:

 


void QWidget::focusInEvent(QFocusEvent *event)
void QWidget::focusOutEvent(QFocusEvent *event)

 

The reimplementation would need to do whatever needs to be done, and then ‘rethrow’ the event to the handler in the base-class (‘QLabel::‘).

In my little exercise, I am not willing to do this for one reason:

  • Presently, the one menu-item is also the only item in the same menu. Therefore, that item can be the only one selected in the menu, while the given menu is being shown. Therefore, typing <Enter> will always trigger that one entry.

If I did implement focus-indication, then the graphics that result might also detract from the simplicity, with which the one menu-item is only highlighted, while the mouse is hovering over it. According to that, my item would also be highlighted because it has received focus. I could implement fuzzier logic, by which a mouse-hover event will change the highlight, but by which typing <Tab> will change the same highlight back, to whichever menu-item has newly received focus.

It would all be a fun game to play, if the one menu contained more than one item.

 

(Update 8/22/2020, 13h40: )

There is a basic truth to how a ‘QWidgetAction’ object behaves, which this exercise and my supplementary reading have taught me. If the object is only used as a menu-item that can be operated in one way – by clicking on it and typing <Enter>, then there is only the complicated way to highlight from its widget, that it will be the target of the key-press, that I described blow, and then implemented. In that case, one does as I did, and makes the object the source of a Signal, which is connected to a Slot.

However, if the object is not connected to a Slot, then widgets within it can be operated as those widgets normally would. I.e., a check-box is checkable, a ‘QLabel’ widget with its own mnemonic letter will send focus to its Buddy if that letter is typed… An apparent menu-entry that is checkable will not cause the menu to close, when its widget has been checked…

Yet, when the ‘QWidgetAction’ object’s Signal is connected to a Slot somewhere, it’s still possible to ‘fill in’ additional data, displayed by its contained widget, and then to signal to this object that its action is to be carried out, by typing <Enter>. The Slot may then assume that all the details have been filled in by the user…

However, this way of using the object is undermined, if its widget calls the function:

 


setAttribute(Qt::WA_Hover, true);

 

As suggested in the code snippets above. The default use of the menu-item requires that it can be clicked on, thus becoming selected, but not activated, until the user has filled in the data of its associated widget, and typed <Enter>. And so, the first approximation I made above, of highlighting the widget if the mouse is hovering over it, is not really consistent with how ‘QWidgetAction’ will be used 90% of the time.

But alternatively, the function may be called, presumably in the constructor, and then the ‘QLabel::enterEvent(QEvent*)‘ method overridden, to call ‘QWidgetAction::hover()‘ explicitly, just so that the menu-item will become selected, because hovered over with the mouse.

 

(Update 8/22/2020, 15h25: )

In a parallel fashion, without having to set any special flags in the constructor, the widget may override the ‘QLabel::mousePressEvent(QMouseEvent*)‘ method, so that it calls the ‘QWidgetAction::trigger()‘ method, assuming that the derived object has a (non-NULL) pointer to the suitable ‘QWidgetAction’ object as a property.

 


 

(Update 8/21/2020, 19h25: )

Technically, the problem can be solved, of not just highlighting the ‘QLabel’ -derived widget of a ‘QWidgetAction’ object, only when hovered over by the mouse, but also, when being highlighted by the keyboard’s <Tab> or <Shift>+<Tab> keys.

Just as I had overridden the ‘.enterEvent()‘ and ‘.leaveEvent()‘ methods, as an alternative, the subclass of ‘QLabel’ can be given a Slot, that receives the Signal generated by ‘QMenu’, which is:

 


void QMenu::hovered(QAction*)

 

Every time this Slot receives its Signal, it can test the argument, to see if that is equal to the address of the ‘QWidgetAction’ object. But, here’s what would make that tricky:

The ‘QLabel’ -derived object is not itself the ‘QWidgetAction’ object! Therefore, the ‘QLabel’ -derived object would need to hold a property that points to one. In addition, the constructor would need to receive such a pointer, which means that the ‘QWidgetAction’ object would actually need to be created first. Its address could then be passed in to the constructor of the ‘QLabel’ -derived class, to initialize this property.

If the argument of the signal equals this property, the ‘QLabel’ -derived object can set the style-sheet that corresponds to being highlighted. Otherwise, it would set the style-sheet that corresponds to ‘not being highlighted’, and do so every time the said signal is received.

In addition to that, the actual statements need to be executed, to ‘connect()‘ this (‘QMenu’ -originated) Signal to the Slot, of every ‘QLabel’ -derived object which is to be managed in this way. Computational overhead would take place, due to Slots receiving the signal, but not corresponding to menu entries being hovered. More overhead would result, due to Slots that are already highlighted, but receiving the command to make themselves highlighted again.

That all seems too complex a procedure to follow, just to make it easier for the user to select these custom menu-entries, and not have to do so using the mouse.


 

 

(Update 8/21/2020, 21h00: )

I have now incorporated this last concept, into the version of my exercise which is published in the compressed files ‘Creator_Test3.tar.gz‘ and ‘Creator_Test3.zip‘. In doing so, I have learned something interesting about how ‘QWidgetAction’ objects behave in real life, when used with ‘QLabel’ widgets.

Just when these menu entries are hovered over with the mouse, They are not in fact focussed. It’s only when hovered over in this way, and either the <Enter> Key is typed once, or, the mouse is Left-Clicked, that the menu entry comes into focus. Then, when the <Enter> Key is pressed, the selected menu-entry is activated.

 


 

(Update 8/21/2020, 23h45: )

1:)

It turns out that this problem is easy to solve. All one needs to do is to call:

 


'setShortcutContext(Qt::ApplicationShortcut)'

 

On the ‘QWidgetAction’ object. Problem solved.

 


 

(Update 8/22/2020, 12h20: )

Now, a curious reader might ask the question next – as I do, of myself – What needs to be done, if the single ‘QWidgetAction’ instance is to contain more than one child widget. I.e., instead of just containing a spinner or a combobox, the intended, single menu-action could contain both ‘a user-name field’, and ‘a password field’, which need to be filled in, before <Enter> is pressed, thus causing a single Slot to become activated. In such a case, the way to add child-widgets where the context called for a single widget is:

  • Create a dummy ‘QWidget’ object, using a constructor-call that has no arguments. This dummy widget will have the same logical role that the ‘central widget’ had, in my published exercise,
  • Create a Layout Manager, with a constructor-call that has the dummy widget as its only argument,
  • Create the child widgets, using a constructor-call that is not to have the dummy widget as its argument,
  • Add each child widget to the layout manager. Doing so will reparent them,
  • Call the ‘setDefaultWidget()‘ method of the ‘QWidgetAction’ object once, to set the dummy widget created above.

 


 

(Update 8/22/2020, 17h40: )

Given the observation that some readers seem to be a bit reluctant to download my compressed files, I will paste the relevant, updated version of my Menu Code here…

The file ‘menubar.h‘ –

 


#ifndef MENUBAR_H
#define MENUBAR_H

#include "mainwindow.h"

class SimpleMenu : public QWidget
{

public:
    SimpleMenu(MainWindow *parent = 0);

};

class HoverWidget : public QLabel
{
public:
    HoverWidget(QWidget *parent = 0, QAction *menu_action = 0);

    void enterEvent(QEvent*);

public slots:
    void receive_hover(QAction*);

protected:
    void mousePressEvent(QMouseEvent*);

private:
    QAction *causer;
    bool is_highlighted;
};


#endif // MENUBAR_H

 

The file ‘menubar.cpp‘ –

 


#include "menubar.h"
#include "mainwindow.h"
#include <QApplication>
#include <QMenu>
#include <QMenuBar>
#include <QWidgetAction>
#include <QFont>

SimpleMenu::SimpleMenu(MainWindow *parent)
    : QWidget(parent)
{

    QAction *quit = new QAction("&Quit", this);
    quit->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q));

//    QAction *reset_window = new QAction(this);
//    reset_window->setText("Re&set Program!");
//    reset_window->setShortcut(Qt::CTRL + Qt::Key_0);

    QAction *zoom_in = new QAction("Zoom I&n", this);
    QAction *zoom_out = new QAction("Zoom Ou&t", this);

    zoom_in->setShortcut(Qt::CTRL + Qt::Key_Plus);
    zoom_out->setShortcut(Qt::CTRL + Qt::Key_Minus);

    QMenuBar *toplev = new QMenuBar(parent);

    QMenu *file;
    file = toplev->addMenu("&File");
    file->addAction(zoom_in);
    file->addAction(zoom_out);
    file->addSeparator();
    file->addAction(quit);

    QMenu *help;
    help = toplev->addMenu("&Help");

    QWidgetAction *reset_window = new QWidgetAction(this);
    HoverWidget *reset_window_label = new HoverWidget(help, reset_window);
    reset_window_label->setText("<i>Reset Program</i> &nbsp; &nbsp; Ctrl+0");
    reset_window->setDefaultWidget(reset_window_label);
    reset_window->setShortcut(Qt::CTRL + Qt::Key_0);
    reset_window->setShortcutContext(Qt::ApplicationShortcut);

    help->addAction(reset_window);
    help->addAction(quit);

    //  Qt5 Semantics for Signals sent to Slots...

    connect(quit, &QAction::triggered, qApp, &QApplication::quit);

    connect(reset_window, &QAction::triggered, parent, &MainWindow::resetQ);
    connect(zoom_in, &QAction::triggered, parent, &MainWindow::zoom_in_do);
    connect(zoom_out, &QAction::triggered, parent, &MainWindow::zoom_out_do);

    connect(help, &QMenu::hovered, reset_window_label, &HoverWidget::receive_hover);

}


HoverWidget::HoverWidget(QWidget *parent, QAction *menu_action) :
    QLabel(parent)
{
    causer = menu_action;
    is_highlighted = false;
    setTextFormat(Qt::RichText);
    setStyleSheet("QLabel {color: blue; margin: 4px}");
    QFont font = this->font();
    font.setPointSize(11);
    font.setFamily("Noto Sans");
    setFont(font);
//    setFixedWidth(200);

    setAttribute(Qt::WA_Hover, true);

}

void HoverWidget::receive_hover(QAction *id)
{
    if (id == causer && is_highlighted == false) {
        setStyleSheet("QLabel {color: blue; background-color: yellow; margin: 4px}");
        is_highlighted = true;
    } else if (id != causer && is_highlighted == true) {
        setStyleSheet("QLabel {color: blue; margin: 4px}");
        is_highlighted = false;
    }
}

void HoverWidget::enterEvent(QEvent *event)
{
    if (causer != 0)
        causer->hover();

    QLabel::enterEvent(event);
}

void HoverWidget::mousePressEvent(QMouseEvent*)
{
    if (causer != 0)
        causer->trigger();
}



 

Enjoy,

Dirk

Print Friendly, PDF & Email

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>