18

What is the best way, to detect if a C# Winforms application has been idle for a certain period of times?

If a user decides to ALT+TAB and do some work with Microsoft Word or whatever for 30 minutes, leaving our app unused, I want our app to self-terminate.

This is the accepted answer for a similar question: Check if an application is idle for a time period and lock it

However, the answer is pertinent to Windows being idle for a period of time, not a specific application. I want our application to terminate if it's not been used for say, 30 minutes.

I looked at this:

http://www.codeproject.com/Articles/13756/Detecting-Application-Idleness

However I read in the comments that this doesn't work for multi-threaded applications, of which our app is one. Our app has a main form, which spawns modal and modeless forms, which use Async Await to fill grids etc.

Then I looked at SetWindowsHookEx, unsure if that would work.

Surely someone has a solution (compatible with .NET 4.5 hopefully ) :)

TIA

Community
  • 1
  • 1
Scott
  • 924
  • 3
  • 8
  • 28
  • 1
    Winform onfocus lost can be used in combination with a timer. – schultz Apr 05 '16 at 16:25
  • you need to measure that with performance counters and then decide depending on which numbers you will consider that your application has been idle – Zinov Apr 05 '16 at 19:30
  • If I saw an application self-terminate I would assume it crashed, which would not leave a positive impression... are you sure that that's the behavior you want? – Pieter Witvoet Apr 05 '16 at 19:40

2 Answers2

23

There are many ways to do it and the answer somewhat depends on what you need to do. You are clear and specific about what you need. The following is something I developed that probably would fit your requirements. What it is doing is using Application.Idle to determine when the application has finished processing messages then it sets a timer and filters (listens to) all messages for the application and if a relevant message (such as mouse or keyboard) is received then it resets the timer. It ignores mouse move since it is possible to move the mouse over the application without using the application. It has been a while since I wrote that so I am not sure of the details but I could figure it out if necessary. Note that this is a console program to make the sample easier to try but the code is intended for a forms application.

using System;
using System.Security.Permissions;
using System.Windows.Forms;

namespace _121414
{
    static class Program
    {
        public static Timer IdleTimer = new Timer();
        const int MinuteMicroseconds = 60000;
        static Form1 f = null;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            LeaveIdleMessageFilter limf = new LeaveIdleMessageFilter();
            Application.AddMessageFilter(limf);
            Application.Idle += new EventHandler(Application_Idle);
            IdleTimer.Interval = MinuteMicroseconds;    // One minute; change as needed
            IdleTimer.Tick += TimeDone;
            IdleTimer.Start();
            f = new Form1();
            Application.Run(f);
            Application.Idle -= new EventHandler(Application_Idle);
        }

        static private void Application_Idle(Object sender, EventArgs e)
        {
            if (!IdleTimer.Enabled)     // not yet idling?
                IdleTimer.Start();
        }

        static private void TimeDone(object sender, EventArgs e)
        {
            IdleTimer.Stop();   // not really necessary
            MessageBox.Show("Auto logoff");
            f.Close();
        }

    }

    [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public class LeaveIdleMessageFilter : IMessageFilter
    {
        const int WM_NCLBUTTONDOWN = 0x00A1;
        const int WM_NCLBUTTONUP = 0x00A2;
        const int WM_NCRBUTTONDOWN = 0x00A4;
        const int WM_NCRBUTTONUP = 0x00A5;
        const int WM_NCMBUTTONDOWN = 0x00A7;
        const int WM_NCMBUTTONUP = 0x00A8;
        const int WM_NCXBUTTONDOWN = 0x00AB;
        const int WM_NCXBUTTONUP = 0x00AC;
        const int WM_KEYDOWN = 0x0100;
        const int WM_KEYUP = 0x0101;
        const int WM_MOUSEMOVE = 0x0200;
        const int WM_LBUTTONDOWN = 0x0201;
        const int WM_LBUTTONUP = 0x0202;
        const int WM_RBUTTONDOWN = 0x0204;
        const int WM_RBUTTONUP = 0x0205;
        const int WM_MBUTTONDOWN = 0x0207;
        const int WM_MBUTTONUP = 0x0208;
        const int WM_XBUTTONDOWN = 0x020B;
        const int WM_XBUTTONUP = 0x020C;

        // The Messages array must be sorted due to use of Array.BinarySearch
        static int[] Messages = new int[] {WM_NCLBUTTONDOWN,
            WM_NCLBUTTONUP, WM_NCRBUTTONDOWN, WM_NCRBUTTONUP, WM_NCMBUTTONDOWN,
            WM_NCMBUTTONUP, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, WM_KEYDOWN, WM_KEYUP,
            WM_LBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONDOWN, WM_RBUTTONUP,
            WM_MBUTTONDOWN, WM_MBUTTONUP, WM_XBUTTONDOWN, WM_XBUTTONUP};

        public bool PreFilterMessage(ref Message m)
        {
            if (m.Msg == WM_MOUSEMOVE)  // mouse move is high volume
                return false;
            if (!Program.IdleTimer.Enabled)     // idling?
                return false;           // No
            if (Array.BinarySearch(Messages, m.Msg) >= 0)
                Program.IdleTimer.Stop();
            return false;
        }
    }
}
Sam Hobbs
  • 2,594
  • 3
  • 21
  • 32
  • Ive built something based on this. What was the SecurityPermission attribute for? On the MSDN site it says [This API is now obsolete](https://learn.microsoft.com/en-us/dotnet/api/system.security.permissions.securitypermissionattribute?view=netframework-4.8) It seems to work without it and doesnt require "allow unsafe code" for the project – t0b4cc0 Mar 23 '20 at 09:08
  • @t0b4cc0 you need to ask Microsoft. They tend to say things are obsolete without an explanation and sometimes the documentation is not obsolete. In this case, however, I think you simply need to scroll down to the remarks in the page. – Sam Hobbs Mar 23 '20 at 20:38
  • Thank you! I have noticed that when I call "MessageBox.Show", the idle timer doesn't work (i.e. no timeout while the message box is open). So I've added a wrapper which calls IdleTimer.Start() before showing a message box. – Robin Oct 23 '20 at 12:06
1

You would need to decide what "idle" means. I assume no mouse or key input received by the application during the required time.

You could add a message filter that will see all application-wide Windows messages. See Application.AddMessageFilter. Check for the message codes for key down and mouse down, and record the last time that they occur, but return false so that all messages are handled as normal by the runtime. Likely codes are:

WM_LBUTTONDOWN = 513
RBUTTONDOWN = 516
WM_KEYDOWN = 256

Then in a separate timer elsewhere check when the last event is more than 30 minutes ago.

There are also potential issues with "terminating" the application. If the application has modal forms open, it sounds as if it might not be in a particularly safe state to kill abruptly.

Stuart Whitehouse
  • 1,421
  • 18
  • 30
  • IIRC - you might have a problem in that message filters are thread specific. Not sure that will work for OP since he specifically stated that he need something that will work across a multi-thread app. –  Apr 05 '16 at 22:06
  • Although the OP is running background tasks on other threads, my understanding was that Windows messages would still arrive on the original thread – Stuart Whitehouse Apr 05 '16 at 22:09
  • Well I provided a sample. It would be easy to try it. Most threads would not have a UI (message loop). The message filter would probably need to be used for every other thread that has it's own UI (message loop). If there could be **relevant** activity in a thread that is not UI activity then it would need to be detected separately. – Sam Hobbs Apr 05 '16 at 22:13