Convert QKeySequence of QKeySequenceEdit to Native Windows Virtual Key Codes VK_*

In a blog post in 2020, I described how to utilize the WinAPI SendInput() function to emulate hotkey presses to automatically take game screenshots for my video game reviews. While I intended to create a simple GUI application to do the task, I ended up with only a hack because of a massive boulder that Windows threw in my way. Or after me, chasing me down a narrow path.

Forget the boulder.

(Although it would be a fitting metaphor to describe Windows: tall, fat, and destructive to user privacy.)

I wanted a simple input field where the user can press a key sequence that will be executed repeatedly at an interval. Qt conveniently provides QKeySequenceEdit for this purpose, and when I tried to insert the Xbox Game Bar hotkey, it did not register. Well, it did, in that Windows took a screenshot. But it was not recorded by the widget. Windows seems to intercept and eat the key presses. That was when I decided to just hard-code my needs and call it a day.

Two years later, I figured that it was about damn time to fix this, and this is where I ran into issues with the translation of key codes from QKeySequenceEdit and QKeySequence to native Windows virtual key codes.

This is where our adventure begins.

Qt is a cross-platform toolkit, and as such, its developers created an abstraction of a platform’s keyboard presses and how the keys are represented within the framework. You have several types to contend with.

On the other end of the spectrum is the old and creaky Win32 API that knows nothing about Qt or enums and handles keys as a gigantic list of #define statements.

The key “A” is represented as Qt::Key_A with an associated value of 0x41. This key does not even have a name in Windows, although its value also is 0x41. That is because it falls within the standard ASCII range. What about “F12”? Well, you have Qt::Key_F12 with a value of 0x0100003B and VK_F12 with a value of 0x7B in Windows. Additionally, Qt separates modifier keys like SHIFT or CONTROL from regular ones, and Windows does not do that in its “data types”.

How do you combine those two worlds and make them become BFFs 👩‍❤️‍👨?

(By crying. A lot.)

How to Make it Work

Note: Links to my own code are subject to changes I make in the code. There may be broken references, so I included the relevant snippets here.

Based on my research, I found two options:

  1. Write your own mapper.
  2. Intercept QKeyEvent and extract the goods from there via its native*() accessor methods.

I started looking at the first solution but found it quite the hassle, given that in Qt’s internals, there is a mapper (the best link I could find), but you cannot use it in your own code. It is a viable blueprint for your own implementation, though.

Thence (to use a funny word), I opted for door number 2️⃣. But even the event with its precious native*() methods wasn’t the holy grail.

The first step to victory was intercepting the keyPressEvent(QKeyEvent*) method to even get to the exciting stuff. To do that, I subclassed QKeySequenceInput and added a bit of my own magic. First, I crippled the support for multiple key sequences (line 57). I do not require that, and it makes the widget less confusing to users.

auto seq = keySequence();
if (0 != seq.count()) {
    setKeySequence(seq[0]);
}

Second, and here is the infamous and dreaded “but” when it comes to solutions, the native modifiers aren’t actually that native. As it turns out, it is yet another Qt enum of which I found the values in an internal header file. I hard-coded them into a helper method to convert from Qt internal values to Windows public values (line 72). Fun stuff. Many portable.

At this point, I felt like I was the Vault Hunter in the Borderlands 2 game, being chased around the map from objective to objective to objective only to get a simple water pump going or something menial like that.

QSet<quint32> convertToWinModifier(int qtNativeModifier) const {
    auto winModifier = QSet<quint32>{};
    
    // ShiftAny
    if (0x00000011 & qtNativeModifier) {
        winModifier.insert(0xA0); // VK_LSHIFT;
    }
    // ControlAny
    if (0x00000022 & qtNativeModifier) {
        winModifier.insert(0xA2); // VK_LCONTROL
    }
    // AltAny
    if (0x00000088 & qtNativeModifier) {
        winModifier.insert(0x5B); // VK_LWIN
    }
    // MetaAny
    if (0x00000044 & qtNativeModifier) {
        winModifier.insert(0x12); // VK_MENU
    }
    
    return winModifier;
}

The value returned from nativeVirtualKey() appears correct, though. As seen with the “F12” example earlier, Qt and Windows use different values, but the key sequence ALT+F12 successfully triggered for me.

What I am essentially doing in this event handler is storing the native Windows virtual key codes that are entered in my KeyInputEdit subclass. They are later picked up by the MainWindow and merged with modifier and regular keys the user can select via checkboxes. See the Walkthrough section of the documentation to learn how the application works.

By combining an input widget with a manual selection of a handful of keys, I probably managed to cover all the (reasonable) keyboard shortcuts that one can think of (given western languages). If a hotkey cannot be entered into the input field for whatever reason, you can construct it in another way.

I store and convert the “special modifiers” in the class SpecialModifiers and the “special key(s)” in SpecialKeys. The first spits out a set of native Windows key codes.

QSet<quint32> combinedCode() const {
    auto modifiers = QSet<quint32>{};
    if (shift) {
        modifiers.insert(0xA0); // VK_LSHIFT;
    }
    if (control) {
        modifiers.insert(0xA2); // VK_LCONTROL
    }
    if (meta) {
        modifiers.insert(0x5B); // VK_LWIN
    }
    if (alt) {
        modifiers.insert(0x12); // VK_MENU
    }
    return modifiers;
}

The latter only a single key code because hotkeys only support one key combined with none, one, or more modifiers.

int code() const {
    if (print) {
        return 0x2C; // VK_SNAPSHOT
    }
    return 0x00;
}

I went with the key codes instead of the names so I wouldn’t have to include the windows.h file in a simple data class. This is a point for potential future refactoring.

Merging the native key codes recorded by the input widget with the manual selection stored in SpecialModifiers and SpecialKeys happens in HotkeyItem (line 29). This class transports the information from the UI’s source to the executor that calls the SendInput() function.

void
HotkeyItem::mergeNativeSequenceAndSpecials(
        const SpecialModifiers& modifiers,
        const SpecialKeys& keys) 
{
    // Duplicates will be filtered by the QSet.
    nativeSequence.modifiers.unite(modifiers.combinedCode());
    // Only one key can be active. This is enforced by the UI. OR-ing both
    // values delivers either the "normal" key or the "special" one.
    nativeSequence.key |= keys.code();
}

If you have the option of intercepting a QKeyEvent, then this guide should provide you with all the tools necessary to utilize Qt’s keyboard handling system with native Windows functions.

A short note on naming: this code was conceived in a matter of two days while chasing issues and trying to find solutions to weird problems. I realize that class and function names and the organization of the code might not always be optimal right now. There was only so much I could do while restructuring the internals to adapt to one new challenge after the next. Let’s just say I left some fun for later 🤓.

I hope this was helpful. Thank you for reading.

One thought on “Convert QKeySequence of QKeySequenceEdit to Native Windows Virtual Key Codes VK_*

Leave a Reply to I Bestow Upon Thee “HotkeyAutoExecute”, My Game Screenshot Automator – The Codeslinger Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.