Activity Monitor
| Have you ever wondered how much you work in a day? You might keep track of when you start and stop, but do you really keep track of every interruption? In this article, learn about how to keep track of user activity and see how to build a component to add to the Visual Studio toolbox. |
| Arian Kulp Difficulty: Intermediate Time Required: 1-3 hours Cost: None Hardware: None Download: |
Introduction
I’ve built time-tracking applications before, but I’m not that great at using them. It seems that I just can’t remember to pause and resume the timer consistently enough. If I keep it logged in while I run to the store, it’s not all that accurate!
This article talks about the Win32 API call that you can use to get a handle on user activity. To be more useful, I decided to also add the necessary glue to expose the features as a component to add to your applications from the Visual Studio toolbox, much as a Timer or StatusBar control is used. This makes it easy to wire up the properties and events with less coding later.
To run this sample, you will need to have Visual Studio 2005 Express Edition installed (either Visual C# or Visual Basic version). An archive containing the full source code and a pre-compiled EXE is linked from the top of the article. Feel free to use this as it is or to expand it as you see fit.
Is Anyone There?
So the big question is: how do you know if the user is active? At some level, Windows must know, since the screensaver is based on idle time, but how does it know? If you think about it, user activity is really only measurable by user input. If the mouse is being clicked or the keyboard pressed, the user is clearly active. How do we know if the mouse is being clicked or the keyboard pressed? Why Windows messages, of course!
You’ve probably hooked into form events to detect if it’s closing, moved, or if a drag-and-drop operation is occurring. You can also hook into events to see mouse movements and key presses. Unfortunately, this is limited to events within your form and its controls. Once the mouse moves out of the form, it’s no longer visible. As it turns out, you can hook into events at the system level as well, though you’ll see a lot more coming through. This isn’t as straight-forward from managed code. It is possible though. On the other hand, if filtering system messages isn’t to your liking, there’s always the GetLastInputInfo method!
Visual Basic
<DllImport("User32.dll")> _
Private Shared Function GetLastInputInfo(ByRef plii As LASTINPUTINFO) As Boolean
End Function
Visual C#
[DllImport("User32.dll")]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
This wonderfully simple function populates a structure with the timestamp of the last time a mouse or keyboard message occurred at the system level. With this in hand, you just need to decide how to determine when the user is truly idle. For example, if the last user input timestamp was 2 seconds ago, is the user idle? Maybe they are reading a dialog and are about to click. Has it been a minute? Maybe they are reading a PDF that they just opened. The definition of idle depends on what the scenario is. Keeping track of the user’s active/idle state is more than just calling GetLastInputInfo.
To be more comprehensive, I also added a timer to the class to determine how much time the user has spent in the Active state, when the timer was started (or reset), a feature to enable/disable/reset the timer, and properties to determine how much time to consider until a user is inactive. It also raises events when the user switches between active and idle states. Bundling it into a component simplifies the main code and makes it easier to add the relevant features.
Visual Basic
Private Sub GetLastInput(ByVal userState As Object)
GetLastInputInfo(Me.lastInput)
Me._lastActivity = Me.lastInput.dwTime
If (Environment.TickCount - Me.lastInput.dwTime) > Me._idleThreshold Then
If Me._userActiveState <> UserActivityState.Inactive Then
Me._userActiveState = UserActivityState.Inactive
Me.activityStopWatch.Stop()
Me.RaiseUserIdleEvent()
End If
ElseIf Me._userActiveState <> UserActivityState.Active Then
Me._userActiveState = UserActivityState.Active
Me.activityStopWatch.Start()
Me.RaiseUserActiveEvent()
End If
End Sub
Visual C#
private void GetLastInput(object userState)
{
GetLastInputInfo(ref this.lastInput);
this._lastActivity = this.lastInput.dwTime;
if ((Environment.TickCount - this.lastInput.dwTime) > this._idleThreshold)
{
if (this._userActiveState != UserActivityState.Inactive)
{
this._userActiveState = UserActivityState.Inactive;
this.activityStopWatch.Stop();
this.RaiseUserIdleEvent();
}
}
else if (this._userActiveState != UserActivityState.Active)
{
this._userActiveState = UserActivityState.Active;
this.activityStopWatch.Start();
this.RaiseUserActiveEvent();
}
}
Another Tool in the ToolBox
Components in the Visual Studio Toolbox fit into categories: components and controls. It’s a pretty fine line – essentially a control is a component with a user interface. Components are the controls that aren’t visible at runtime and appear beneath the form at design time. By creating a user activity component, you can drag it from the Toolbox to the form, set properties and wire up events in the Properties pane, and keep the form code as uncluttered as possible.
Creating a component isn’t much more work than creating any well-encapsulated class. In fact, I originally created the UserActivityTimer class like any other class. I just realized that it made more sense to create it as a component for better reuse.
The first step is to extend the System.ComponentModel.Component class. Already, your class (if public) will show up in the Toolbox when you rebuild the project. It will also show your public properties and events when dragged to a form, though it’s not a very complete view. Provided you have setup your class properly with properties and events, you will have a pretty easy time finishing your work. Adding attributes to the class, properties, and events help you to create an experience closer to the Microsoft-supplied components/controls. Unless specified, all attributes are in the System.ComponentModel namespace.
| Attribute |
Level |
Usage |
Description |
|
System.Drawing.ToolboxBitmap |
Class |
ToolboxBitmap(typeof(UserActivityTimer), "images.Control.ico") |
Specifies a bitmap for this component to show in the toolbox |
|
DefaultProperty |
Class |
DefaultProperty("IdleThreshold") |
The bolded property to which the Properties pane defaults |
|
DefaultEvent |
Class |
DefaultEvent("UserActive") |
The bolded event to which the Properties pane defaults |
|
Description |
Property/ Event |
Description("User-defined data associated with the object." |
The description that appears in the lower part of the Properties pane |
|
Bindable |
Property |
Bindable(true) |
Indicates if property is used for data binding and raises an event when the value changes |
|
DefaultValue |
Property |
DefaultValue("") or DefaultValue(1000) |
The value that will show in the Properties pane if no change is made. This value is also bolded when set to indicate that it hasn’t been overridden |
|
Category |
Property |
Category("Data") |
Specifies the grouping within the Properties pane (Data,General, Behavior) |
|
Browsable |
Property |
Browsable(false) |
Whether or not it should show at all. Good for properties that make no sense at design-time |
|
TypeConverter |
Property |
TypeConverter (typeof(StringConverter)) |
Since the Properties pane allows entries as string, the TypeConverter handles back-and-forth |
Visual Basic
<DefaultValue(False)> _
<Category("Behavior")> _
<Description("The current state of the timer.")> _
Public Property Enabled() As Boolean
Get
Return Me._timerEnabled
End Get
Set(ByVal value As Boolean)
' If in design-time change the value but not the actual state
If Not Me.DesignMode Then
If value = True Then
Enable()
Else
Disable()
End If
Else
Me._timerEnabled = value
End If
End Set
End Property
Visual C#
[DefaultValue(false)]
[Category("Behavior")]
[Description("The current state of the timer.")]
public bool Enabled
{
get
{
return this._timerEnabled;
}
set
{
// If in design-time change the value but not the actual state
if (!this.DesignMode)
{
if (value == true) Enable();
else Disable();
}
else
{
this._timerEnabled = value;
}
}
}
Note also, the check to the DesignMode property. This comes from inheriting from the Component class. This is important, because when a developer sets properties in the Visual Studio designer, the object actually gets its properties set. Often, you don't really want to take any action in design mode. This is how you tell the difference.
With the component built, its properties show up in the Properties pane just like any other component:
Figure 1 - The component's properties
Putting the Control to Work
With a component in place, it’s much easier to create an application around it. For this sample, I decided to create a simple user interface to expose the information. It doesn’t expose all information, but it’s a good sampling of useful data. You can enable or disable the timer from the notification icon in the system tray.
Figure 2 - User Interface at runtime
All information shown is obtained through properties of the component. A standard Timer component is used to update the UI. Formatting the time properly is manual work, and the number of seconds must be multiplied by 1000 to convert it to milliseconds. The events are raised from the component which runs on its own thread. For this reason, it’s not possible to directly set the UI controls when the event fires without causing a threading exception. There are two ways to solve this.
You could delegate the call to the form’s thread, as is done in the sample. This adds a small amount of complexity in code and clarity, but is a pretty common solution, and with proper code comments anyone should be able to grasp it. The problem with this method is that a flood of events will cause a flood of UI updates. This might not be very efficient.
Another way is to update state variables in the form class when events fire. Then, when the form’s UI update timer fires, it could use the state variables to determine what to show. There would be potential issues with threading concurrency if the event fires at the same moment as the Timer executes, but this can either be handled with locks, or ignored at the expense of occasionally inaccurate information. This also reduces UI updates to the Timer’s update interval regardless of how often events fire.
Visual Basic
Private Sub updateTimer_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles updateTimer.Tick
Dim ts As TimeSpan = userTimer.ActiveTime
' Not necessary to update the status label since the active/idle events to it
statusLabel.Text = userTimer.UserActiveState.ToString()
Dim totalActive As String = String.Format("{0:00}:{1:00}:{2:00}", ts.Hours, ts.Minutes, ts.Seconds)
timerLabel.Text = String.Format("{0} since {1}", totalActive, userTimer.LastResetTime.ToShortTimeString())
appNotifyIcon.Text = String.Format("Coding 4 Fun - User Activity - Active {0}", totalActive)
End Sub
Visual C#
private void updateTimer_Tick(object sender, EventArgs e)
{
TimeSpan ts = userTimer.ActiveTime;
// Not necessary to update the status label since the active/idle events to it
statusLabel.Text = userTimer.UserActiveState.ToString();
string totalActive = string.Format("{0:00}:{1:00}:{2:00}",
ts.Hours, ts.Minutes, ts.Seconds);
timerLabel.Text = string.Format("{0} since {1}",
totalActive, userTimer.LastResetTime.ToShortTimeString());
appNotifyIcon.Text = string.Format("Coding 4 Fun - User Activity - Active {0}", totalActive);
}
Next Steps
This application isn’t terribly useful as it is, but it could be a good foundation for a time tracking application. Adding a few fields to select a project would let you keep track of time spent. You could use the Enable/Disable properties to let a user pause the timer, and the Reset method to switch projects.
Another purpose would be in corporate development to track user productivity to a fine level. With a higher interval it might also serve as a good way to close unneeded resources such as network/database connections when a user isn’t actively using an application anyway. You could achieve the same effect when the screensaver kicks in, but this makes it easy to use an independent threshold. Just drop the UserActivityTimer onto a form, set the IdleThreshold property, and wire up some actions to the events. Hopefully it’s intuitive enough to put to use quickly.
Conclusion
In this article, I’ve shown how to compute total user activity time and keep track of the user’s state with events using a Win32 API call that returns the last keyboard/mouse input event at the system level. This functionality is then bundled into a component for easy use in other applications. The sample application exposes this information to test it out and demonstrate how to use it.
I threw it together in order to keep better track of my own time, but hopefully it will be useful for other projects as well. If you haven’t yet, download Visual Studio 2005 Express Edition for Visual C# or Visual Basic and have fun with it!
|
Arian Kulp is an independent software developer and writer working in the Midwest. He has been coding since the fifth grade on various platforms, and also enjoys photography, nature, and spending time with his family. Arian can be reached through his web site at http://www.ariankulp.com. |