Underc0de

Programación General => C# - VB.NET => Mensaje iniciado por: Solid Water en Octubre 11, 2018, 03:53:38 PM

Título: Cerrando programa WinForms tras 15 minutos de inactividad
Publicado por: Solid Water en Octubre 11, 2018, 03:53:38 PM
Cerrar programa winforms tras 15 minutos de inactividad por parte del usuario.
No parece tan difícil pero veamos como después se complica un poco.
Supongamos que tenemos un formulario padre con 2 botones que llama a los formularios hijos:

(https://i.imgur.com/jyBEl6U.png)

Código (csharp) [Seleccionar]

   private void button1_Click(object sender, EventArgs e)
        {
            Form form2 = new Form2();
            form2.Show();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Form form3 = new Form3();
            form3.Show();
        }




Bueno para controlar los 15 minutos utilizaremos un timmer que agregaremos al formulario 1 y setearemos que cada 1 minuto suba 1 punto el contador al llegar a 15 minutos sin actividad cerraríamos el programa.

En el form1 load declaramos que el timmer llamara a la función TimerEventProcessor cada 1 minuto:
Código (csharp) [Seleccionar]

            timer1.Interval = 60000;
            timer1.Tick += new EventHandler(TimerEventProcessor);
            timer1.Start();


Y en la función nuestro contador aumentará de 1 en 1 y si llega a 15 cerrará el programa:

Código (csharp) [Seleccionar]

private void TimerEventProcessor(object sender, EventArgs e)
        {
            _counter++;
           
            if (_counter == 15)
            {
               
                Environment.Exit(1);
                this.Close();
            }
        }


Cabe destacar que la variable counter es un static int declarado como dato miembro de form1.

Código (csharp) [Seleccionar]

static int _counter;


Bueno todo muy lindo, pero ahora faltaría que cada vez que se presione una tecla o se haga click en algun control de cualquiera de los formularios la variable counter vuelva a 0 así vuelven a contarse los 15 minutos desde la última vez que hubo actividad del usuario.

Para eso debemos delegar a los controles la función que vuelve el contador a 0.

Código (csharp) [Seleccionar]

public static void mdiPrincipal_Click(object sender, EventArgs e)
{
            _counter = 0;
            MessageBox.Show("Click or keypressed");
}


Como estamos testeando le puse a la función un messagebox que me avisa que hubo un click o key pressed.
Pero todavía nos falta delegarle dicha función, a los eventos click y keypress de todos los controles de los formularios.
Sin lugar a dudas hacerlo 1 por 1 sería una solución horrible.
Para delegar la función a los eventos keypressed y click podemos usar un código como el siguiente:

Código (csharp) [Seleccionar]

foreach (Control control in Controls)
{

control.Click += new System.EventHandler(mdiPrincipal_Click);
        control.KeyDown += new KeyEventHandler(mdiPrincipal_Click);
}


Pero el código tiene un problema, cuando el control con el que se encuentra el foreach es un groupBox, este tiene
anidado los controles dentro de el, por lo que se saltiaría todos los controles dentro de groupboxs.
Para solucionarlo podemos decir que si es un group box, recorra los elementos que hay dentro de este último 1 por 1 como lo hace con los de los formularios. El siguiente código funcionaría en caso de que haya como máximo 2 group box (anidados) o cualquier cantidad sin anidar en el formulario:

Código (csharp) [Seleccionar]

foreach (Control control in Controls)
            {
                if (control is GroupBox)
                {
                    foreach (Control controlinside in control.Controls)
                    {
                        if (controlinside is GroupBox)
                        {
                            foreach (Control controlinside2 in controlinside.Controls)
                            {
                                controlinside2.Click += new System.EventHandler(mdiPrincipal_Click);
                                controlinside2.KeyDown += new KeyEventHandler(mdiPrincipal_Click);
                            }
                        }
                        else
                        {

                            controlinside.Click += new System.EventHandler(mdiPrincipal.mdiPrincipal_Click);
                            controlinside.KeyDown += new KeyEventHandler(mdiPrincipal.mdiPrincipal_Click);
                        }
                    }
                }
                else
                {

                    control.Click += new System.EventHandler(mdiPrincipal.mdiPrincipal_Click);
                    control.KeyDown += new KeyEventHandler(mdiPrincipal.mdiPrincipal_Click);
                }
            }


Pero que pasaría si hubiera más de 2 group box no podemos hardcodear todo así. Debemos hacerlo de un modo más prolijo.
Para eso creé una función que utiliza recursividad con los group box volviendo a llamarse una y otra vez siempre y cuando encuentre otro group box anidado.
En conclusión creé 2 funciones la principal (AddEvents) que "si es un group box llama a la recursiva" y si no agrega los eventos a cada control, y la recursiva que se encarga de los group box y group box anidados en otros group box.

Código (csharp) [Seleccionar]

public static void AddEvents(Form form){
             
            foreach (Control control in form.Controls)
            {
               
                if(recursiva(control)){

                }
                else
                {

                    control.Click += new System.EventHandler(mdiPrincipal_Click);
                    control.KeyDown += new KeyEventHandler(mdiPrincipal_Click);
                }
            }
       }


        public static bool recursiva(Control control){

             if (control is GroupBox)
             { 
                   foreach (Control controlinside in control.Controls)
                   {
                       if (recursiva(controlinside))
                        {


                         }else{

                           controlinside.Click += new System.EventHandler(mdiPrincipal_Click);
                           controlinside.KeyDown += new KeyEventHandler(mdiPrincipal_Click);
                        }
                   }

                  return true;

              }else{

                return false;

          }

        }


Por último, nos queda en todos los form loads, llamar a:
Código (csharp) [Seleccionar]

AddEvents(this);


Como podrán ver para testearlo en un formulario anidé muchos groupbox con controles dentro:

(https://i.imgur.com/hz3g2S6.png)

Más tarde les dejo el proyecto para descargar que ahora no estoy en casa.

Autor: Solid Water (Matías).

PD: Seguramente podría hacerse de algún modo con hooking o capturar algún evento más, pero bueno todo depende de para que queramos usarlo, podría ser mejor o peor.

Saludos,
Título: Re:Cerrando programa WinForms tras 15 minutos de inactividad
Publicado por: 79137913 en Noviembre 16, 2018, 08:56:10 AM
HOLA!!!

Admiro tu creatividad, pero con un 10% de ese codigo lo haces XD

Tenes que usar un invoke sobre "getlastuserinput" y listo te dara la ultima interaccion del usuario.

Te dejo documentacion con ejemplos y todo:
http://www.pinvoke.net/default.aspx/user32.getlastinputinfo

GRACIAS POR LEER!!!
Título: Re:Cerrando programa WinForms tras 15 minutos de inactividad
Publicado por: Solid Water en Noviembre 16, 2018, 09:20:21 AM
Gracias por la información, cuando busqué una solución a eso no la encontré por eso hice todo eso.

Sin embargo al parecer esa función que mencionas devuelve la última acción sobre el sistema y no sobre la aplicación, y hay que agregar más código para comparar con la aplicación incluso un timmer:

https://stackoverflow.com/questions/3289438/wpf-application-idle-time (https://stackoverflow.com/questions/3289438/wpf-application-idle-time)

Saludos,
Título: Re:Cerrando programa WinForms tras 15 minutos de inactividad
Publicado por: Solid Water en Noviembre 16, 2018, 11:20:34 AM
Mira Lo hice utilizando esa función:

User32Interop.cs
Código (csharp) [Seleccionar]

using System;
using System.Text;
//For use DLL import:
using System.Runtime.InteropServices;

namespace WindowsFormsApplication2
{
/*This class only tells me when the system has been idle, not the application.
If the user clicks into Word and works there for an hour,
I still want a timeout. To handle this case, I simply will remember when my application
loses focus by overriding the OnDeactivated and OnActivated methods
on the application object.
  */
    public static class User32Interop
    {
        public static TimeSpan GetLastInput()
        {
            var plii = new LASTINPUTINFO();
            plii.cbSize = (uint)Marshal.SizeOf(plii);
            if (GetLastInputInfo(ref plii))
                return TimeSpan.FromMilliseconds(Environment.TickCount - plii.dwTime);
            else
                throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
        }

        [DllImport("user32.dll", SetLastError = true)]
        static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
        struct LASTINPUTINFO
        {
            public uint cbSize;
            public uint dwTime;
        }
    }
}


Form1.cs
Código (csharp) [Seleccionar]

using System;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApplication2
{
    public partial class Form1 : Form
    {
        //Variables miembro que utilizaremos:
        public static DateTime? _lostFocusTime;
        int _counter = 0;

        public Form1()
        {
            InitializeComponent();
        }


        //Si respuesta llega a 15 (minutos) cerraremos el programa
        public void Timmer_Function(object sender, EventArgs e)
        {
            bool respuesta = IsIdle;
            //MessageBox.Show(IsIdle.ToString());
            if (respuesta)
            {
                _counter++;

            }
            else
            {
                _counter = 0;
            }

            if (_counter == 15)
            {
                Environment.Exit(1);
                this.Close();
            }

        }


        //LLamaremos al timmer cada 1 minuto:
        private void Form1_Load(object sender, EventArgs e)
        {
            timer1.Interval = 60000;
            timer1.Tick += new EventHandler(Timmer_Function);
            timer1.Start();
        }

     

        //En todos los forms debemos sobreescribir estas 2 funciones:
        //Para saber cuando nuestra aplicación pierde el foco.
        override protected void OnDeactivate(EventArgs e)
        {
            _lostFocusTime = DateTime.Now;
            base.OnDeactivate(e);
        }

        protected override void OnActivated(EventArgs e)
        {
            _lostFocusTime = null;
            base.OnActivated(e);
        }


        /*The IsIdle routine was added to the application object.
         * It handles the global case where the app has focus but nothing happened
         * (IsMachineIdle) and the specific case where the application lost focus while
         * the user is doing other stuff (isAppIdle ):*/
        public bool IsIdle
        {
            get
            {
                TimeSpan activityThreshold = TimeSpan.FromMinutes(1);
                TimeSpan machineIdle = User32Interop.GetLastInput();
             
                TimeSpan? appIdle = _lostFocusTime == null ? null :
                                   (TimeSpan?)DateTime.Now.Subtract
                                             (_lostFocusTime.Value);

                bool isMachineIdle = machineIdle > activityThreshold;
                bool isAppIdle = appIdle != null && appIdle > activityThreshold;
                return isMachineIdle || isAppIdle;
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Form2 form2 = new Form2();
            form2.Show();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Form3 form3 = new Form3();
            form3.Show();
        }
    }
}


Y en todos los otros forms sobreescribir las funciones así:

Código (Csharp) [Seleccionar]
override protected void OnDeactivate(EventArgs e)
        {
            Form1._lostFocusTime = DateTime.Now;
            base.OnDeactivate(e);
        }

        protected override void OnActivated(EventArgs e)
        {
            Form1._lostFocusTime = null;
            base.OnActivated(e);
        }


No sé si sean menos líneas, de hecho tampoco que sea menor trabajo a nivel procesador.
Pero me agrada tener otra forma de hacerlo.
Luego lo escribo de una forma más detallada.

Saludos,