domingo, 18 de julio de 2010

WPF: Obtener datos del sistema de energía de Windows 7

Hola a todos de nuevo!

En mi opinión uno de los grandes retos que tenemos los desarrolladores hoy en día (entre muchos otros) es aprovechar todas las ventajas que nos ofrece el sistema operativo e intentar que nuestra aplicación se mimetice con el mismo de tal forma que la linea de separación entre aplicación y S.O. se desdibuje lo máximo posible, de esa forma, nuestros usuarios tendrán menos trabajo a la hora de acostumbrarse a nuestra aplicación, pues la forma de usarla, y la forma en que esta se comporta les será mucho más familiar.


En este sentido Windows 7 ha sido un gran avance, poniendo a nuestra disposición un gran número de características que podemos incorporar en nuestra aplicación como la gestión de energía, las jumplist, los Thumbnails con toolbars, las barras de progreso tras el icono de la barra de tareas y muchas otras más.

Voy a centrar algunos artículos sobre estos temas, Windows 7 es el S.O. de más rápida adopción de la Historia, con 150 millones de licencias vendidas y estos números hacen que centrarse en incorporar características propias de el sea una apuesta segura para nosotros y una gran ventaja tanto para nuestra aplicación como para nuestros usuarios.

He decidido comenzar con la gestión de energía, cada vez más los portátiles ganan terreno en el ámbito empresarial y en poco tiempo veremos una explosión en tablets equipadas con Windows 7, con lo que nuestras aplicaciones se empezarán a mover a estos nuevos escenarios donde una cosa prima frente a todas: la duración de la batería.

Debemos hacer que nuestras aplicaciones sean amigables con la energía, tenemos que comprender que el escritorio y el laptop / tablet son dos escenarios distintos e intentar evitar consumir toda la vida de la batería en un tiempo reducido, haciendo que nuestras aplicaciones conozcan el estado actual de la batería del dispositivo sobre el que se ejecutan podremos tomar decisiones en concordancia con el % de energía disponible e incluso sugerir a nuestro usuario que modifique la forma en que está usando nuestra aplicación para maximizar la vida de la misma.

Con este objetivo en mente, vamos a empezar, primero echadle un vistazo a nuestra aplicación terminada:
App
Realmente es mucha información con la que podemos jugar, sabemos el tipo de fuente de energía que estamos consumiendo, si tenemos sistemas de alimentación ininterrumpida, la capacidad máxima y actual de la batería, y las alertas de cargas criticas (cuando el S.O. se va a cerrar) o mínimas (cuando empieza a avisarnos) y por supuesto el % restante de la batería.

Vamos a comenzar, lo primero es crear un nuevo proyecto WPF Application (File > New > Project), una vez creado, vamos a diseñar la pantalla principal en el archivo MainWindow.xaml
Los estilos que he usado en esta aplicación están disponibles en la descarga en el archivo Application.xaml, si tenéis dudas acerca de como usar / crear estilos, echadle un vistazo a mis dos artículos sobre este tema: aquí y aquí

Los controles usados son muy básicos, la batería que se ve en la esquina superior derecha es una progressbar, textblocks para el texto, dos checkbox, un tabcontrol y grids para ordenar todo un poco, aquí podéis ver el XAML:
<Grid>
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="Black">
</GradientStop>
<GradientStop Offset="1" Color="DarkGray">
</GradientStop>
</LinearGradientBrush>
</Grid.Background>
<Grid.RowDefinitions>
<RowDefinition Height="50"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Name="BateriaPercent" FontSize="24" 
Text="Batería restante (%):" VerticalAlignment="Center" 
HorizontalAlignment="Left" Foreground="White" 
Margin="6,0,0,0"/>
<ProgressBar Height="40" Margin="245,5,12,0" Name="pBarEnergia" 
VerticalAlignment="Top" Value="0" />
<TabControl Grid.Row="1" Margin="12,6,12,12" Name="TabControl1">
<TabItem Header="Información de energía" Name="TabItem1">
<Grid>
<Grid.Background>
<ImageBrush ImageSource="pack://application:,,,/WPF Power Management;component/Info.png" 
Stretch="Uniform" Opacity=".2"/>
</Grid.Background>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".4*"></ColumnDefinition>
<ColumnDefinition Width=".6*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height=".1*"></RowDefinition>
<RowDefinition Height=".1*"></RowDefinition>
<RowDefinition Height=".1*"></RowDefinition>
<RowDefinition Height=".7*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Name="tbFuente" Text="Fuente de energía:" 
VerticalAlignment="Center" HorizontalAlignment="Left" Foreground="White" 
Margin="6,0,0,0"/>
<TextBlock Grid.Row="0" Grid.Column="1" Name="tbFuenteText" VerticalAlignment="Center" 
HorizontalAlignment="Left" Foreground="White" Margin="6,0,0,0"/>
<TextBlock Grid.Row="1" Grid.Column="0" Name="tbBateriaPresent" Text="Batería presente:" 
VerticalAlignment="Center" HorizontalAlignment="Left" Foreground="White" 
Margin="6,0,0,0"/>
<CheckBox  Grid.Row="1" Grid.Column="1" Name="chkBateriaPresente" HorizontalAlignment="Left" 
Height="20" VerticalAlignment="Center"></CheckBox>
<TextBlock Grid.Row="2" Grid.Column="0" Name="tbUPSPresent" Text="UPS presente:" 
VerticalAlignment="Center" HorizontalAlignment="Left" Foreground="White" 
Margin="6,0,0,0"/>
<CheckBox  Grid.Row="2" Grid.Column="1" Name="chkUpsPresente" HorizontalAlignment="Left" 
Height="20" VerticalAlignment="Center"></CheckBox>
<TextBlock Grid.Row="3" Grid.Column="0" Name="tbEstadoBateria" Text="Estado de la batería:" 
VerticalAlignment="Top" HorizontalAlignment="Left" Foreground="White" Margin="6,0,0,0"/>
<TextBlock Grid.Row="3" Grid.Column="1" Name="tbEstado" VerticalAlignment="Top" 
HorizontalAlignment="Left" Foreground="White"/>
</Grid>
</TabItem>
</TabControl>
</Grid>


Si lo miráis con detenimiento veréis que no es nada complicado.


Para trabajar con el sistema de energía de Windows 7 debemos jugar con las apis de win32 (no manejadas) de windows, por ello, lo mejor es crear una nueva clase separada que se encargue de todo el trabajo de importar y llamar a las funciones necesarias del api y nos devuelva la información que necesitamos.


Para esto necesitaremos los InteropServices, por lo que lo mejor es importar este namespace en esta nueva clase:


Imports System.Runtime.InteropServices

Public Class PowerManagement
'...
'...
'...
End Class


Ahora debemos empezar a definir las constantes necesarias para que nuestro código sea más legible en esta clase:


'Esta constante identifica el valor devuelto por CallNtPowerInformation
'si el acceso a este método nos fue negado por seguridad.
Const STATUS_ACCESS_DENIED As UInteger = 3221225506

'Estas constantes definen el tipo de información que solicitamos en el 
'método no manejado CallNtPowerInformation se usan en el parámetro InformationLevel.
Const SYSTEM_POWERCAPABILITIES As Integer = 4 'Capacidades de energía del sistema.
Const SYSTEM_BATTERYINFO As Integer = 5 'Estado de la batería.


Como podéis ver tenemos 3 constantes, esto es lo que significa cada una:


Constante
Descripción
STATUS_ACCESS_DENIEDEste valor es devuelto por el método CallNtPowerInformation si no tenemos suficientes permisos para ejecutarlo.
SYSTEM_POWERCAPABILITIESEste valor indica al método CallNtPowerInformation que deseamos obtener las capacidades de energía del sistema.
SYSTEM_BATTERYINOEste valor indica al método CallNtPowerInformation que deseamos obtener información sobre la batería del sistema.


A continuación debemos definir 2 estructuras que contendrán los valores devueltos por el método CallNtPowerInformation.


La primera se llama SystemPowerCapabilities y contendrá toda la información con las capacidades de energía del sistema:


<StructLayout(LayoutKind.Sequential)>
Structure SystemPowerCapabilities
<MarshalAs(UnmanagedType.I1)>
Public PowerButtonPresent As Boolean
<MarshalAs(UnmanagedType.I1)>
Public SleepButtonPresent As Boolean
<MarshalAs(UnmanagedType.I1)>
Public LidPresent As Boolean
<MarshalAs(UnmanagedType.I1)>
Public SystemS1 As Boolean
<MarshalAs(UnmanagedType.I1)>
Public SystemS2 As Boolean
<MarshalAs(UnmanagedType.I1)>
Public SystemS3 As Boolean
<MarshalAs(UnmanagedType.I1)>
Public SystemS4 As Boolean
<MarshalAs(UnmanagedType.I1)>
Public SystemS5 As Boolean
<MarshalAs(UnmanagedType.I1)>
Public HiberFilePresent As Boolean
<MarshalAs(UnmanagedType.I1)>
Public FullWake As Boolean
<MarshalAs(UnmanagedType.I1)>
Public VideoDimPresent As Boolean
<MarshalAs(UnmanagedType.I1)>
Public ApmPresent As Boolean
<MarshalAs(UnmanagedType.I1)>
Public UpsPresent As Boolean
<MarshalAs(UnmanagedType.I1)>
Public ThermalControl As Boolean
<MarshalAs(UnmanagedType.I1)>
Public ProcessorThrottle As Boolean
Public ProcessorMinThrottle As Byte
Public ProcessorMaxThrottle As Byte
<MarshalAs(UnmanagedType.I1)>
Public FastSystemS4 As Boolean
Public spare2_1 As Byte
Public spare2_2 As Byte
Public spare2_3 As Byte
<MarshalAs(UnmanagedType.I1)>
Public DiskSpinDown As Boolean
Public spare3_1 As Byte
Public spare3_2 As Byte
Public spare3_3 As Byte
Public spare3_4 As Byte
Public spare3_5 As Byte
Public spare3_6 As Byte
Public spare3_7 As Byte
Public spare3_8 As Byte
<MarshalAs(UnmanagedType.I1)>
Public SystemBatteriesPresent As Boolean
<MarshalAs(UnmanagedType.I1)>
Public BatteriesAreShortTerm As Boolean
Public granularity As Integer
Public capacity As Integer
End Structure


Como podéis observar hay mucha más información que la que yo he escogido para mostrar, realmente podemos controlar de una forma milimétrica el consumo energético y las capacidades técnicas en cuanto a energía de la máquina en la que nos encontramos.


El decorador de la estructura <StructureLaout(LayoutKind.Sequential)> indíca al interop que los campos de la misma se ordenarán de forma consecutiva tal y como los hemos expresado aquí. El decorador <MarshalAs(UnmanagedType.I1)> le indica al interop de que forma debe trabajar con el tipo de dato a continuación (boolean en todos los casos) para facilitar la conversión de información entre tipos de datos .NET (manejados) y Win32 (no manejados).


La segunda estructura es SystemBatteryState y contendrá toda la información referente a la batería de nuestro equipo:


<StructLayout(LayoutKind.Sequential)>
Structure SystemBatteryState
<MarshalAs(UnmanagedType.I1)>
Public AcOnLine As Boolean
<MarshalAs(UnmanagedType.I1)>
Public BatteryPresent As Boolean
<MarshalAs(UnmanagedType.I1)>
Public Charging As Boolean
<MarshalAs(UnmanagedType.I1)>
Public Discharging As Boolean
Public spare1 As Byte
Public spare2 As Byte
Public spare3 As Byte
Public spare4 As Byte
Public MaxCapacity As UInteger
Public RemainingCapacity As UInteger
Public Rate As UInteger
Public EstimatedTime As UInteger
Public DefaultAlert1 As UInteger
Public DefaultAlert2 As UInteger
End Structure


Esta es bastante más pequeña que la anterior y usa los mismos tipos de decorados.


Para terminar de declarar el acceso a Win32 solo nos falta importar la función necesaria desde la Dll de win32 encargada de las tareas de gestión de energía powrprof.dll:





<DllImport("powrprof.dll", SetLastError:=True)>
Private Shared Function CallNtPowerInformation(ByVal InformationLevel As Int32, 
ByVal lpInputBuffer As IntPtr, 
ByVal nInputBufferSize As UInt32,
ByVal lpOutputBuffer As IntPtr, 
ByVal nOutputBufferSize As UInt32) As UInt32
End Function


Esta función CallNtPowerInformation será la que usaremos para comunicarnos con Windows 7 y obtener todos los datos de energía que necesitemos. Tiene 5 parámetros, el 2º y 3º parámetro no son importantes, puesto que no queremos enviarle información, tan solo recibir, el 2º se envía con Nothing y el 3º se envía 0, en cuanto al primer parámetro, aquí es donde usaremos las dos constantes definidas más arriba (SYSTEM_) para indicarle a la función que tipo de información estamos solicitando, el 4º parámetro es un puntero a la variable en la que queramos almacenar la información recibida y el 5º es el tamaño actual de esa variable.


Bien, ahora que hemos terminado de declarar todo lo necesario para trabajar con el api de win32, vamos a crear dos nuevos métodos en esta clase que se encarguen de obtener la información que deseamos.


En primer lugar creamos el método GetPowerCapabilities con este código:


Public Shared Function GetPowerCapabilities() As SystemPowerCapabilities
Dim PowerCapabilities As SystemPowerCapabilities
Dim Status As IntPtr = IntPtr.Zero
Dim ReturnValue As UInteger
Try
Status = Marshal.AllocCoTaskMem(Marshal.SizeOf(GetType(SystemPowerCapabilities)))
ReturnValue = CallNtPowerInformation(SYSTEM_POWERCAPABILITIES, Nothing, 0, Status, 
Marshal.SizeOf(GetType(SystemPowerCapabilities)))
If ReturnValue = STATUS_ACCESS_DENIED Then
MessageBox.Show("Su usuario no tiene permisos de acceso suficientes para obtener 
información sobre el sistema de energía.")
Return Nothing
End If
PowerCapabilities = Marshal.PtrToStructure(Status, GetType(SystemPowerCapabilities))
Catch ex As Exception
Finally
Marshal.FreeCoTaskMem(Status)
End Try
Return PowerCapabilities
End Function


Vamos a revisar con detenimiento el código de este método, si no has trabajado antes con funciones del win32 desde .NET verás un par de cosas que te sonarán a chino, sobre todo las que empiezan por Marshal.


Marshal es una clase del namespace InteropServices que nos facilita el trabajo con métodos no manejados, es muy completa y tiene muchos métodos, en nuestro caso usamos 4:


Método
Descripción
Marshal.AllocCoTaskMem

Nos sirve para reservar un bloque de memoria del asignador de tareas COM del tamaño indicado y nos devuelve un puntero al bloque asignado.
Marshal.FreeCoTaskMemCon este método liberamos un bloque de memoria obtenido con anterioridad con el método AllocCoTaskMem.
Marshal.PtrToStructureCopiamos el contenido del bloque de memoria al que apunta el puntero que le indicamos a una estructura para poder acceder a los datos.
Marshal.SizeOfDevuelve el tamaño en Bytes de un objeto, sin tener en cuenta la sobrecarga de los tipos manejados, nos devuelve el tamaño no manejado, que es el necesario para las funciones no manejadas.


En esta función usamos la tercera constante que habíamos declarado (STATUS_ACCESS_DENIED) si el resultado de la llamada a CallNtPowerInformation devuelve este valor, significa que nuestro usuario no tiene permisos para realizar esta llamada.


En último lugar, debemos crear el segundo método que interactuará con el API de win32, GetBatteryInformation:


Public Shared Function GetBatteryInformation() As SystemBatteryState
Dim Status As IntPtr = IntPtr.Zero
Dim BattStatus As SystemBatteryState
Dim ReturnValue As UInteger
Try
Status = Marshal.AllocCoTaskMem(Marshal.SizeOf(GetType(SystemBatteryState)))
ReturnValue = CallNtPowerInformation(SYSTEM_BATTERYINFO, Nothing, 0, Status, 
Marshal.SizeOf(GetType(SystemBatteryState)))
If ReturnValue = STATUS_ACCESS_DENIED Then
MessageBox.Show("Su usuario no tiene permisos de acceso suficientes para obtener 
información sobre el estado de la batería.")
Return Nothing
End If
BattStatus = Marshal.PtrToStructure(Status, GetType(SystemBatteryState))
Catch ex As Exception
Finally
Marshal.FreeCoTaskMem(Status)
End Try
Return BattStatus
End Function


El código es básicamente el mismo, si te fijas bien los únicos cambios son el primer parámetro de la función CallNtPowerInformation y el tipo de estructura que queremos obtener, en este caso lo que obtenemos es todos los detalles sobre la batería del sistema, carga máxima, carga actual, fuente de energía, etc…


Bueno, con esto hemos terminado el trabajo con el API de Win32, ahora solo nos queda algo más de código en nuestra ventana principal de WPF para poder hacer que todo esto funcione.


Empecemos con las declaraciones de variables, necesitamos 3 variables globales de la clase y privadas:


Private MySPC As PowerManagement.SystemPowerCapabilities
Private MyBatt As PowerManagement.SystemBatteryState
Private WithEvents Tmr As New DispatcherTimer


Básicamente, definimos una variable por cada estructura necesaria, SystemPowerCapabilities y SystemBatteryState y un control DispatcherTimer que será el encargado de refrescar constantemente la información del sistema de energía.


En el evento Load, configuraremos el Timer y comprobaremos la versión de sistema operativo sobre la cual nos estamos ejecutando, ten en cuenta que este código es para Windows 7 y lo más probable es que falle en algunos puntos en XP o Vista:


'Comprobamos que estemos en un Windows 7:
If Environment.OSVersion.Version.Major = 6 And 
Environment.OSVersion.Version.Minor > 0 Then
Tmr.Interval = New TimeSpan(0, 0, 1)
Tmr.IsEnabled = True
Else
MessageBox.Show("Es necesario windows 7 
para poder ejecutar este programa.")
End If


Este es un código muy sencillo, sobre todo después del empacho de Interop y Win32 que nos hemos dado anteriormente jeje, simplemente comprobamos que estemos ejecutando Windows 7, que a pesar de su nombre es la versión 6.1 del sistema operativo, por lo que comprobamos que estemos en un sistema superior al 6.0, es muy posible que el próximo windows soporte este código, así que lo mejor es buscar un sistema cuya versión sea superior a 6.0 (Windows Vista). Una vez hecho esto configuramos el intervalo del timer a 1 segundo y lo habilitamos.


El control DispatcherTimer no dispara el evento Elapsed como el Timer de siempre de Windows Forms, en este caso debemos manejar el evento Tick:


Private Sub Tmr_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles Tmr.Tick
Tmr.IsEnabled = False
MySPC = PowerManagement.GetPowerCapabilities()
'Comprobamos si existe batería en el equipo:
If MySPC.SystemBatteriesPresent = True Then
SetValues()
End If
Tmr.IsEnabled = True
End Sub


En este caso cuando recibimos el evento lo primero que hacemos es desactivar el timer, ya que no queremos que se dispare de nuevo hasta que hayamos terminado, llamamos a nuestro método GetPowerCapabilities y comprobamos si el equipo contiene baterías, si es así llamamos a un método SetValues que se encarga de pasar los valores obtenidos a nuestros controles de visualización y por último volvemos a habilitar el timer para que siga su ciclo de refresco normal.


El último método que necesitamos se llama SetValues, simplemente obtiene los valores de energía necesarios y los pasa a los controles para que los visualicemos:


Private Sub SetValues()
tbFuenteText.Text = If(MyBatt.AcOnLine = True, "Corriente Alterna", "Batería")
chkBateriaPresente.IsChecked = MySPC.SystemBatteriesPresent
chkUpsPresente.IsChecked = MySPC.UpsPresent
'Obtenemos información de la batería
MyBatt = PowerManagement.GetBatteryInformation()
pBarEnergia.Value = Math.Round((MyBatt.RemainingCapacity * 100) / MyBatt.MaxCapacity, 0)
tbEstado.Text = "Corriente externa: "+If(MyBatt.AcOnLine=True,"SI","NO")+vbCrLf+ 
"Capacidad Máxima: "+MyBatt.MaxCapacity.ToString+"mWh"+vbCrLf+
"Capacidad Actual: "+MyBatt.RemainingCapacity.ToString+"mWh"+vbCrLf+
"Alerta carga minima: "+MyBatt.DefaultAlert2.ToString+"mWh"+vbCrLf+
"Alerta carga crítica: "+MyBatt.DefaultAlert1.ToString+"mWh"
End Sub


Y con esto, si compilamos nuestra aplicación (y estamos trabajando en Windows 7, con un portatil o tablet) deberíamos ver toda la información de energía que os indique al principio del artículo.


Por si tenéis alguna dificultad, a parte por supuesto de poder contactar conmigo tanto dejando mensajes como en mi twitter, como en msdn o a mi email directamente, os dejo el proyecto completo para descarga, junto con los estilos y control templates que he usado.


Y recordad, con un simple comentario, podéis hacer feliz a un pequeño bloguero :)


Muchas gracias por leerme y Happy Coding!

No hay comentarios:

Publicar un comentario