C Windows API 判断用户是否在一段时间内不活跃

C Windows API determine if user inactive for certain period

所以我创建了一个带有阻塞消息事件循环的基本程序(等待时几乎不使用 CPU)并等待用户更改前景 window,然后执行一些代码:

#include <Windows.h>

VOID ExitFunction()
{
    // Do Something
}

BOOL WINAPI HandlerRoutine(DWORD dwCtrlType)
{
    switch (dwCtrlType)
    {
        case CTRL_SHUTDOWN_EVENT:
            ExitFunction();
            return TRUE;
        case CTRL_LOGOFF_EVENT:
            ExitFunction();
            return TRUE;
            //default:
                //We don't care about this event
                //Default handler is used
    }
    return FALSE;
}

VOID CALLBACK WinEventProcCallback(HWINEVENTHOOK hWinEventHook, DWORD dwEvent, HWND hwnd, LONG idObject, LONG idChild, DWORD dwEventThread, DWORD dwmsEventTime)
{
    if (dwEvent == EVENT_SYSTEM_FOREGROUND)
    {
        // Do Stuff
    }
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    MSG msg;
    HWINEVENTHOOK WindowChangeEvent;

    SetConsoleCtrlHandler(HandlerRoutine, TRUE);
    WindowChangeEvent = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, NULL, WinEventProcCallback, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);

    while (GetMessage(&msg, NULL, 0, 0, PM_REMOVE) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    ExitFunction();

    return 0;
}

我还想合并检查用户是否在一定时间内处于非活动状态(没有 mouse/keyboard 输入),但要保持低资源使用率。我可以想到几种方法来解决这个问题:

可以这样调用:

LASTINPUTINFO li;
li.cbSize = sizeof(LASTINPUTINFO);
GetLastInputInfo(&li);

记住最低的资源使用率,什么方法最好在activity检查中实施mouse/keyboard,同时检查前景window变化。

您可以设置一个计时器(请参阅 SetTimer)以在任意超时到期时调用用户定义的回调。这使您可以跳出阻塞 GetMessage 循环。

回调可以检查最后一次输入的时间戳,并将其与当前时间戳进行比较。如果该时间间隔超过所需的不活动超时,它可以执行必要的步骤。否则它会在超时的剩余时间重新启动计时器。

以下代码说明了这一点:

#include <Windows.h>

#include <iostream>


static const DWORD timeout_in_ms { 5 * 1000 };


void TimeoutExpired() { std::wcout << L"Timeout elapsed" << std::endl; }

void CALLBACK TimerProc(HWND, UINT, UINT_PTR id, DWORD current_time)
{
    // Timers are periodic, but we want it to fire only once.
    KillTimer(nullptr, id);

    LASTINPUTINFO lii { sizeof(lii) };
    GetLastInputInfo(&lii);

    auto const time_since_input { current_time - lii.dwTime };
    if (time_since_input < timeout_in_ms)
    {
        // User input was recorded inside the timeout interval -> restart timer.
        auto const remaining_time { timeout_in_ms - time_since_input };
        SetTimer(nullptr, 0, remaining_time, &TimerProc);
    }
    else
    {
        TimeoutExpired();
    }
}

void StartInactivityTimer()
{
    // Start a timer that expires immediately;
    // the TimerProc will do the required adjustments and
    // restart the timer if necessary.
    SetTimer(nullptr, 0, 0, &TimerProc);
}

int wmain()
{
    StartInactivityTimer();

    MSG msg {};
    while (GetMessageW(&msg, nullptr, 0, 0) > 0)
    {
        DispatchMessageW(&msg);
    }
}

整个逻辑包含在TimerProc中。要触发不活动计时器,StartInactivityTimer 会启动一个立即到期的计时器。当 TimerProc 取得控制权时,它会进行所需的计算,然后重新启动计时器,或调用超时程序 TimeoutExpired.

这个实现有两个优点:其一,整个定时器重启逻辑都在一个地方。更重要的是,不活动条件是在第一次调用时评估的。如果在不活动间隔内调用 StartInactivityTimer 时没有任何用户输入,它会立即执行 TimeoutExpired.

另请注意,区间计算使用无符号整数运算,特别是减法。由于无符号整数 'underflow' 在 C 和 C++ 中都有明确定义,因此该解决方案不受 GetTickCount 的 return 值在大约 49.7 天后回绕到 0 的影响。