I recently ran into the same issue as well.
To avoid the situation you mentioned, I tested this on Windows 10. This version still uses the IME-related implementation. I also found an input method based on TSF, which is Rime. Its repository is here:
I also wrote a small test program to print out the message types:
auto msg = MSG{};
while (GetMessageW(&msg, nullptr, 0, 0)) {
if (msg.hwnd == nullptr) {
std::cout << msg.message << "\n";
} else {
DispatchMessageW(&msg);
}
}
I found that when using the Windows 10 IME-related implementation, the corresponding IME notification event does have a message value. The hexadecimal value starts from 0xC000, and the decimal value is 49255. This value is different every time I restart the computer.
This also indicates that the IME notification event has a NULL hwnd.
However, when I switched to Rime, which is based on TSF, I found that there were no corresponding IME notification messages at all. In other words, GetMessage cannot retrieve the event messages related to TSF.
When I changed the code to this:
auto msg = MSG{};
while (GetMessageW(&msg, nullptr, 0, 0)) {
DispatchMessageW(&msg);
}
and simply dispatched all events, the IME switching worked correctly when I pressed the Shift key.
As far as I know, many Windows applications often have two threads, and they use msg.hwnd == NULL to filter out custom messages used internally by the application.
As I mentioned above, IME notification events have a NULL hwnd, so they won't be dispatched by this logic.
If Notepad doesn't work for testing, you could try writing a simple Win32 window with a button that toggles the IME state when clicked. In that case, it won't check msg.hwnd == NULL.
I'm not sure if this helps, but hopefully it gives you another clue about the issue.