What is a proxy action in Qt and how to use it?
Unless you have a very simple UI, there are probably multiple views in your application, all having their own actions. The problem is, not all of these actions are disjunct – there are intersections, and we except to find some of them at particular places in the main menu or access over well-known keyboard shortcuts. The most obvious examples are copy & paste or delete actions, which are usually located under the Edit menu, assigned to standard key combinations (Ctrl+C, Ctrl+V, etc.) and sometimes put on the main toolbar. Creating individual actions for each view leads to multiple menu entries and shortcut conflicts, while creating a single action and managing its state and listening for its signals in individual views leads to complicated code and leaky abstractions. This problem can be solved using a proxy action, action multiplexer or context-aware action – an interesting pattern for Qt-based applications described in this blog post.
To remind, actions are one of the central UI concepts in Qt. Their goal is to provide consistency across different UI elements, especially menus and toolbars. A single instance can be added to the main menu, placed on a toolbar or attached to a key sequence at the same time instead of manual creation and managing of toolbar buttons and menu entries. Once you change its properties, e.g. make an action enabled or disabled, the corresponding menu items and toolbar buttons are updated automatically. Think about a word processing application: selecting some text and pressing Ctrl+B on the keyboard would activate the corresponding action (“toggle the selected text bold”), leading to the corresponding menu entry being checked and the bold “B” button on the toolbar appearing pressed. If you want to learn more about actions, take a look at the QAction’s class documentation.
So how do you solve the problem mentioned at the beginning? An interesting solution I’ve discovered in the Qtilities’ documentation (note, the code below is not based on that library – I didn’t even look at it – and placed in the public domain) is to create a dummy proxy action and use it at the place of real actions. A similar concept also exists in Qt Creator with its commands and action manager. Imagine, you had two views each one providing a “Delete” action. Using a proxy action, you move the intersecting responsibilities to a common third action:
Action | Added to the main menu? | Does the actual work? |
---|---|---|
“Delete” action from view 1 | no | yes |
“Delete” action from view 2 | no | yes |
Proxy action | yes | no |
The proxy action class could be declared like this:
class ProxyAction : public QAction
{
public:
explicit ProxyAction(const QString &text, QObject *parent = nullptr);
QAction *action() const;
void setAction(QAction *action);
private:
void update();
QAction *m_action;
};
The constructor is pretty straightforward. It simply calls the parent constructor and resets the state:
ProxyAction::ProxyAction(const QString &text, QObject *parent)
: QAction(text, parent),
m_action(nullptr)
{
setEnabled(false);
}
The action getter just returns the real action behind the proxy or nullptr
if there is none:
QAction *ProxyAction::action() const
{
return m_action;
}
The action setter is where the magic actually happens. It connects its own triggered
signal with the triggered
signal of the underlying action and subscribes for its updates. It also accepts a nullptr
, allowing to remove all connections and reset the state to its initial value.
void ProxyAction::setAction(QAction *action)
{
if (m_action) {
disconnect(this, &ProxyAction::triggered, m_action, &QAction::triggered);
disconnect(m_action, &QAction::changed, this, &ProxyAction::update);
}
if (action) {
connect(this, &QAction::triggered, action, &QAction::triggered);
connect(action, &QAction::changed, this, &ProxyAction::update);
setEnabled(action->isEnabled());
} else {
setEnabled(false);
}
m_action = action;
}
The last function is the update slot which is called once the underlying action has changed. In our case, we only want to replicate its enabled/disabled state.
You might wonder why the update function isn’t declared as a slot. Since we pass function pointers instead of strings and the method is declared private (we are the only users), we can skip the MOC step for this class and reduce the memory footprint a little bit.
void ProxyAction::update()
{
setEnabled(m_action->isEnabled());
}
That’s it. The usage of this class is very simple. At the place where you usually create your global actions (the main window in the example below, as recommended by the documentation) initialize the proxy action as follows:
void MainWindow::createActions()
{
m_deleteAction = new ProxyAction(tr("&Delete"), this);
m_deleteAction->setIcon(QIcon::fromTheme("edit-delete"));
m_deleteAction->setShortcut(QKeySequence::Delete);
ui->editMenu->addAction(m_deleteAction);
}
It’s up to you to decide how the underlying actions are switched. If you have multiple perspectives like QtCreator does (code editor, form designer, project settings, etc.) you might want to connect it with the switching logic. A good starting point for most applications however is simply listening for a focus change:
void MainWindow::focusChanged(QWidget *old, QWidget *now)
{
if (now == m_viewOne) {
m_deleteAction->setAction(m_viewOne->deleteAction());
} else if (now == m_viewTwo) {
m_deleteAction->setAction(m_viewTwo->deleteAction());
} else {
m_deleteAction->setAction(nullptr);
}
}
The finishing touch by connecting the slot above, for instance in the constructor:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
// ...
createActions();
// ...
connect(qApp, &QApplication::focusChanged, this, &MainWindow::focusChanged);
}
As you can see, I prefer to create the underlying actions in the corresponding views. Usually I even use them directly, for instance in the context menus. But if I want to add them to the menu bar, I do it via a proxy.
Now, you might also wonder, how shortcuts like Ctrl+C / Ctrl+V work out of the box with standard Qt widgets like QTextEdit
even if you have assigned your own actions to these keys. The secret is that they are listening for QEvent::ShortcutOverride
and interrupt the normal event propagation. I wouldn’t do this in my applications, because the architecture becomes very tricky. But it might be a good idea for a public library trying to avoid unnecessary dependencies.