Hello @yezi lan ,
Based on my testing with Microsoft Pinyin, I think the official TSF compartment you are actually looking for is GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION. As you have noticed, when the user presses the Shift key to toggle between Chinese and English, the OPENCLOSE compartment does not change at all. Instead, it seems to toggle the TF_CONVERSIONMODE_NATIVE flag inside the INPUTMODE_CONVERSION compartment. You can find the official definition of these flags in the Predefined Compartments documentation. I think that this is the underlying state that the Language Bar relies on to display "中" or "英".
Regarding the question of why modern IMEs do not seem to expose this state globally through TSF, it seems the issue is not that the IME hides the state, but rather how the TSF architecture handles scope. According to the official Compartments overview documentation, these states are strictly managed on a per-thread basis. So, if you register an ITfCompartmentEventSink in a background C++ Console application, I think it will only receive events for its own isolated console thread. When the user switches to another application (like Chrome or Word) and presses the Shift key, TSF will update the target application's thread compartment, but it does not broadcast this change globally across process boundaries to reach your console application. Because TSF operates this way, building a global monitor purely from an isolated Console process is extremely difficult without injecting DLLs (TSF Text Services) into every running process.
To monitor this state globally without injecting DLLs, there are two alternative methods you can explore. First, you might wonder whether you can use legacy IMM32 APIs as a cross-process workaround. Based on my testing, it seems this legacy method actually still works for classic Win32 applications. You can periodically poll the foreground window, find its default IME window, and send a WM_IME_CONTROL message. Below is a code snippet illustrating the idea:
#include <windows.h>
#pragma comment(lib, "imm32.lib")
// Query the IME state of the active window
void CheckForegroundImeState() {
HWND hForegroundWnd = GetForegroundWindow();
if (!hForegroundWnd) return;
HWND hImeWnd = ImmGetDefaultIMEWnd(hForegroundWnd);
if (!hImeWnd) return;
// Send a cross-process message to query the conversion mode
LRESULT conversionMode = SendMessage(hImeWnd, WM_IME_CONTROL, IMC_GETCONVERSIONMODE, 0);
if (conversionMode & IME_CMODE_NATIVE) {
// IME is in Native/Chinese Mode
} else {
// IME is in Alphanumeric/English Mode
}
}
However, when I tested this IMM32 method with modern Windows 11 applications such as the new Notepad app, the messages seemed to be completely ignored. I suspect the reason is that modern Windows applications might have completely bypassed the legacy IMM32 compatibility layer.
Considering the limitations of IMM32 with modern applications, the idea of using UI Automation is likely a robust method on Windows 11. Because the taskbar visibly updates the state of the Language Bar button for the user to see, you can use the Microsoft UI Automation (UIA) API to act as a "screen reader" for the taskbar. By monitoring the accessible name of the Language Bar button. Below is a code snippet illustrating how you can locate and read this state:
#include <windows.h>
#include <uiautomation.h>
#include <iostream>
#include <string>
// Assuming COM is initialized and pAutomation is your IUIAutomation instance
void CheckTaskbarImeIndicator(IUIAutomation* pAutomation) {
IUIAutomationElement* pRoot = nullptr;
pAutomation->GetRootElement(&pRoot);
// 1. Find the Taskbar based on the class name
IUIAutomationCondition* pTaskbarCond = nullptr;
// ... (Create condition for UIA_ClassNamePropertyId == "Shell_TrayWnd")
IUIAutomationElement* pTaskbar = nullptr;
pRoot->FindFirst(TreeScope_Children, pTaskbarCond, &pTaskbar);
// 2. Find the System Tray Icon for the Input Indicator
// On Win11, the AutomationId is usually "SystemTrayIcon" and its name contains "Input Indicator"
// ... (Use FindAll with TreeScope_Descendants to locate the exact element)
// 3. Read the CurrentName property
BSTR bstrName = nullptr;
if (SUCCEEDED(pIndicatorElement->get_CurrentName(&bstrName)) && bstrName) {
std::wstring indicatorName(bstrName);
// The accessible name will dynamically change when the user presses Shift
if (indicatorName.find(L"Chinese Mode") != std::wstring::npos) {
std::wcout << L"Global state: Chinese Mode" << std::endl;
} else if (indicatorName.find(L"English Mode") != std::wstring::npos) {
std::wcout << L"Global state: English Mode" << std::endl;
}
SysFreeString(bstrName);
}
}
I hope these observations from testing, sample code snippets, and documentation links will provide a clear direction for your project. If you found my response helpful, I would greatly appreciate it if you could provide feedback by following this guide.
Thank you.