WPF inactivity and activity
Asked Answered
N

5

41

I'm trying to handle user inactivity and activity in a WPF application to fade some stuff in and out. After a lot of research, I decided to go with the (at least in my opinion) very elegant solution Hans Passant posted here.

There's only one downside: As long as the cursor stays on top of the window, the PreProcessInput event gets continously fired. I'm having a full-screen application, so this kills it. Any ideas how I can bypass this behaviour would be most appreciated.

public partial class MainWindow : Window
{
    readonly DispatcherTimer activityTimer;

    public MainWindow()
    {
        InitializeComponent();

        InputManager.Current.PreProcessInput += Activity;

        activityTimer = new DispatcherTimer
        {
            Interval = TimeSpan.FromSeconds(10),
            IsEnabled = true
        };
        activityTimer.Tick += Inactivity;
    }

    void Inactivity(object sender, EventArgs e)
    {
        rectangle1.Visibility = Visibility.Hidden; // Update
        // Console.WriteLine("INACTIVE " + DateTime.Now.Ticks);
    }

    void Activity(object sender, PreProcessInputEventArgs e)
    {
        rectangle1.Visibility = Visibility.Visible; // Update
        // Console.WriteLine("ACTIVE " + DateTime.Now.Ticks);

        activityTimer.Stop();
        activityTimer.Start();
    }
}

Update

I could narrow down the described behaviour better (see the rectangle1.Visibility update in the above code). As long as the cursor rests on top of the window and for example the Visibility of a control is changed, the PreProcessInput is raised. Maybe I'm misunderstanding the purpose of the PreProcessInput event and when it fires. MSDN wasn't very helpful here.

Nightie answered 10/2, 2011 at 21:47 Comment(4)
That code works great for me and PreProcessInput isn't raised when mouse is still over the Window. Do you get the same effect if you create a small app with just the code you posted? Which .NET version are you using?Calvinism
@Meleak: Thanks! Indeed, it works with just the above code (shame on me). Anyhow, in my project I still have that strange behaviour. I'm researching and narrowing this down more and will provide more detailed information. For completeness, I'm using .NET 4.Nightie
@Meleak: I've updated the question so that the behaviour is actually comprehensible.Nightie
Try this link #25517005Overcast
N
24

I could figure out what caused the described behaviour.

For example when the Visibility of a control is changed, the PreProcessInput event is raised with PreProcessInputEventArgs.StagingItem.Input of the type InputReportEventArgs.

The behaviour can be avoided by filtering the InputEventArgs for the types MouseEventArgs and KeyboardEventArgs in the OnActivity event and to verify if no mouse button is pressed and the position of the cursor is still the same as the application became inactive.

public partial class MainWindow : Window
{
    private readonly DispatcherTimer _activityTimer;
    private Point _inactiveMousePosition = new Point(0, 0);

    public MainWindow()
    {
        InitializeComponent();

        InputManager.Current.PreProcessInput += OnActivity;
        _activityTimer = new DispatcherTimer { Interval = TimeSpan.FromMinutes(5), IsEnabled = true };
        _activityTimer.Tick += OnInactivity;
    }

    void OnInactivity(object sender, EventArgs e)
    {
        // remember mouse position
        _inactiveMousePosition = Mouse.GetPosition(MainGrid);

        // set UI on inactivity
        rectangle.Visibility = Visibility.Hidden;
    }

    void OnActivity(object sender, PreProcessInputEventArgs e)
    {
        InputEventArgs inputEventArgs = e.StagingItem.Input;

        if (inputEventArgs is MouseEventArgs || inputEventArgs is KeyboardEventArgs)
        {
            if (e.StagingItem.Input is MouseEventArgs)
            {
                MouseEventArgs mouseEventArgs = (MouseEventArgs)e.StagingItem.Input;

                // no button is pressed and the position is still the same as the application became inactive
                if (mouseEventArgs.LeftButton == MouseButtonState.Released &&
                    mouseEventArgs.RightButton == MouseButtonState.Released &&
                    mouseEventArgs.MiddleButton == MouseButtonState.Released &&
                    mouseEventArgs.XButton1 == MouseButtonState.Released &&
                    mouseEventArgs.XButton2 == MouseButtonState.Released &&
                    _inactiveMousePosition == mouseEventArgs.GetPosition(MainGrid))
                    return;
            }

            // set UI on activity
            rectangle.Visibility = Visibility.Visible;

            _activityTimer.Stop();
            _activityTimer.Start();
        }
    }
}
Nightie answered 11/2, 2011 at 14:17 Comment(2)
If I'm using this from within a ViewModel and so don't have access to MainGrid (or form elements), is there another way of detecting the mouse move?Oftentimes
As a follow up to my comment above: I removed the whole if (e.StagingItem.Input is MouseMoveEventArgs) statement, and it's working fine for me, detecting both clicks and mouse moves, while ignoring the mouse simply hovering over the application.Oftentimes
A
51

We've had a similar need for our software... it's a WPF application as well, and as a security feature - a client can configure a time that their user's will be logged off if they are idle.

Below is the class that I made to wrap the Idle Detection code (which utilizes built in Windows functionality).

We simply have a timer tick ever 1 second to check if the idle time is greater than the specified threshold ... takes 0 CPU.

First, here's how to use the code:

var idleTime = IdleTimeDetector.GetIdleTimeInfo();

if (idleTime.IdleTime.TotalMinutes >= 5)
{
    // They are idle!
}

You can use this and also make sure that your WPF full screened app is "focused" to achieve your needs:

using System;
using System.Runtime.InteropServices;

namespace BlahBlah
{
    public static class IdleTimeDetector
    {
        [DllImport("user32.dll")]
        static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);

        public static IdleTimeInfo GetIdleTimeInfo()
        {
            int systemUptime = Environment.TickCount,
                lastInputTicks = 0,
                idleTicks = 0;

            LASTINPUTINFO lastInputInfo = new LASTINPUTINFO();
            lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo);
            lastInputInfo.dwTime = 0;

            if (GetLastInputInfo(ref lastInputInfo))
            {
                lastInputTicks = (int)lastInputInfo.dwTime;

                idleTicks = systemUptime - lastInputTicks;
            }

            return new IdleTimeInfo
            {
                LastInputTime = DateTime.Now.AddMilliseconds(-1 * idleTicks),
                IdleTime = new TimeSpan(0, 0, 0, 0, idleTicks),
                SystemUptimeMilliseconds = systemUptime,
            };
        }
    }

    public class IdleTimeInfo
    {
        public DateTime LastInputTime { get; internal set; }

        public TimeSpan IdleTime { get; internal set; }

        public int SystemUptimeMilliseconds { get; internal set; }
    }

    internal struct LASTINPUTINFO
    {
        public uint cbSize;
        public uint dwTime;
    }
}
Assonance answered 11/2, 2011 at 2:49 Comment(5)
This little beauty saved me a decent amount of coding, thank you. Using windows built in functions is an excellent suggestion.Albuquerque
@Timothy, I am also showing an alert box after being idle for x time to inform user about auto log out. Is there any way I can reset the timer for that alert whenever user is not idle? As of now I need to reset the time for that alert on each activity of a user.Avatar
Your class works great. I use it like that: in windows_loaded I started the timer: DispatcherTimer timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromSeconds(1); timer.Tick += timer_Tick; timer.Start(); and in timer_tick I call this idle class and close the app after 15 min: void timer_Tick(object sender, EventArgs e) { var idleTime = IdleTimeDetector.GetIdleTimeInfo(); if (idleTime.IdleTime.TotalMinutes >= 15) { this.Close(); } }Palmar
This solution will stop working after a certain amount of uptime (~50 days) due to 32-bit tick rollover. The pinvoke website, which this code is probably based on, says: These samples do not take into account the rollover of the tick counter which will occur after ~50 days of uptime. This might be a potentially good workaround this issue.Dedans
the class is checking the idle time across the system, not just the wpf application. Ideally I would like to close the app, if user lets say is on ms word for the last hour, therefore, wpf is not focused. how can I do that?Polypetalous
N
24

I could figure out what caused the described behaviour.

For example when the Visibility of a control is changed, the PreProcessInput event is raised with PreProcessInputEventArgs.StagingItem.Input of the type InputReportEventArgs.

The behaviour can be avoided by filtering the InputEventArgs for the types MouseEventArgs and KeyboardEventArgs in the OnActivity event and to verify if no mouse button is pressed and the position of the cursor is still the same as the application became inactive.

public partial class MainWindow : Window
{
    private readonly DispatcherTimer _activityTimer;
    private Point _inactiveMousePosition = new Point(0, 0);

    public MainWindow()
    {
        InitializeComponent();

        InputManager.Current.PreProcessInput += OnActivity;
        _activityTimer = new DispatcherTimer { Interval = TimeSpan.FromMinutes(5), IsEnabled = true };
        _activityTimer.Tick += OnInactivity;
    }

    void OnInactivity(object sender, EventArgs e)
    {
        // remember mouse position
        _inactiveMousePosition = Mouse.GetPosition(MainGrid);

        // set UI on inactivity
        rectangle.Visibility = Visibility.Hidden;
    }

    void OnActivity(object sender, PreProcessInputEventArgs e)
    {
        InputEventArgs inputEventArgs = e.StagingItem.Input;

        if (inputEventArgs is MouseEventArgs || inputEventArgs is KeyboardEventArgs)
        {
            if (e.StagingItem.Input is MouseEventArgs)
            {
                MouseEventArgs mouseEventArgs = (MouseEventArgs)e.StagingItem.Input;

                // no button is pressed and the position is still the same as the application became inactive
                if (mouseEventArgs.LeftButton == MouseButtonState.Released &&
                    mouseEventArgs.RightButton == MouseButtonState.Released &&
                    mouseEventArgs.MiddleButton == MouseButtonState.Released &&
                    mouseEventArgs.XButton1 == MouseButtonState.Released &&
                    mouseEventArgs.XButton2 == MouseButtonState.Released &&
                    _inactiveMousePosition == mouseEventArgs.GetPosition(MainGrid))
                    return;
            }

            // set UI on activity
            rectangle.Visibility = Visibility.Visible;

            _activityTimer.Stop();
            _activityTimer.Start();
        }
    }
}
Nightie answered 11/2, 2011 at 14:17 Comment(2)
If I'm using this from within a ViewModel and so don't have access to MainGrid (or form elements), is there another way of detecting the mouse move?Oftentimes
As a follow up to my comment above: I removed the whole if (e.StagingItem.Input is MouseMoveEventArgs) statement, and it's working fine for me, detecting both clicks and mouse moves, while ignoring the mouse simply hovering over the application.Oftentimes
P
2

I implements the solution in a IdleDetector class. I have improved a little bit the code. The Iddle detector throw an IsIdle That can be intercepte ! It give that ! I wait for some comments.

public class IdleDetector
{
    private readonly DispatcherTimer _activityTimer;
    private Point _inactiveMousePosition = new Point(0, 0);

    private IInputElement _inputElement;
    private int _idleTime = 300;

    public event EventHandler IsIdle;

    public IdleDetector(IInputElement inputElement, int idleTime)
    {
        _inputElement = inputElement;
        InputManager.Current.PreProcessInput += OnActivity;
        _activityTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(idleTime), IsEnabled = true };
        _activityTimer.Tick += OnInactivity;
    }

    public void ChangeIdleTime(int newIdleTime)
    {
        _idleTime = newIdleTime;

        _activityTimer.Stop();
        _activityTimer.Interval = TimeSpan.FromSeconds(newIdleTime);
        _activityTimer.Start();
    }

    void OnInactivity(object sender, EventArgs e)
    {
        _inactiveMousePosition = Mouse.GetPosition(_inputElement);
        _activityTimer.Stop();
        IsIdle?.Invoke(this, new EventArgs());
    }

    void OnActivity(object sender, PreProcessInputEventArgs e)
    {
        InputEventArgs inputEventArgs = e.StagingItem.Input;

        if (inputEventArgs is MouseEventArgs || inputEventArgs is KeyboardEventArgs)
        {
            if (e.StagingItem.Input is MouseEventArgs)
            {
                MouseEventArgs mouseEventArgs = (MouseEventArgs)e.StagingItem.Input;

                // no button is pressed and the position is still the same as the application became inactive
                if (mouseEventArgs.LeftButton == MouseButtonState.Released &&
                    mouseEventArgs.RightButton == MouseButtonState.Released &&
                    mouseEventArgs.MiddleButton == MouseButtonState.Released &&
                    mouseEventArgs.XButton1 == MouseButtonState.Released &&
                    mouseEventArgs.XButton2 == MouseButtonState.Released &&
                    _inactiveMousePosition == mouseEventArgs.GetPosition(_inputElement))
                    return;
            }

            _activityTimer.Stop();
            _activityTimer.Start();
        }
    }
}
Perlis answered 8/3, 2017 at 13:2 Comment(0)
P
1

Instead of listening to PreProcessInput have you tried PreviewMouseMove?

Ploughshare answered 10/2, 2011 at 23:39 Comment(1)
I believe this is a better alternative. The only issue with it is that it's not directly accessible from a viewmodel like PreProcessInput. Instead you have to bind to it on the MainWindow of you app.Upperclassman
R
0

After digging in Martin Buberl code, I found what I think is a cleaner way to handle the PreProcessInput event for user activity.

InputManager.Current.PreProcessInput += InputManager_PreProcessInput;

I did it by checking the e.StagingItem.Input value for different mouse events and keyboard events:

private void InputManager_PreProcessInput(object sender, PreProcessInputEventArgs e)
{
    switch (e.StagingItem.Input)
    {
        //mouse button pressed
        case MouseButtonEventArgs mbea:
        //mouse wheel used
        case MouseWheelEventArgs mwea:
        //generic mouse event for `MouseMove` event
        case MouseEventArgs mea when (mea.RoutedEvent.Name.Equals(Mouse.MouseMoveEvent.Name)):
        //KeyBoard event
        case KeyEventArgs kea:
            //TODO: manage activity notification
            break;
    }
}

or, without the switch statement:

private void InputManager_PreProcessInput(object sender, PreProcessInputEventArgs e)
{
    if (e.StagingItem.Input is MouseButtonEventArgs
        || e.StagingItem.Input is MouseWheelEventArgs
        || e.StagingItem.Input is KeyEventArgs
        || e.StagingItem.Input is MouseEventArgs mea && mea.RoutedEvent.Name.Equals(Mouse.MouseMoveEvent.Name)
        )
    {
        //TODO: manage activity notification
    }
}
Revenue answered 22/8, 2022 at 14:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.