可以通过使用实现IMessageFilter Interface 的类并使用Application.AddMessageFilter(IMessageFilter) Method 安装此类的实例来监视WinForm 项目中的键盘和鼠标活动。请注意,这种方式只能检测到发布到线程消息队列的消息;幸运的是,所有键盘和鼠标消息都发布到此队列。 (更多信息请见:About Messages and Message Queues。
以下是IMessageFilter 的实现,旨在提供应用程序接收用户输入的时间(TimeUsed 属性)。类构造函数 (Sub New) 采用参数 idleSecondsToIgnore。该参数用于允许在没有用户输入的情况下累积使用时间;将其视为一个宽限期,以说明用户查看应用程序而不主动与其交互的时间。构造函数还处理注册过滤器。
Public Class UsageMonitor : Implements IMessageFilter
Private timeUsedTicksAccumulator As Int64
Private lastActiveTime As DateTime
Private appIsRunning As Boolean
Private ReadOnly idleTicksToIgnore As Int64
Public Sub New(idleSecondsToIgnore As Int32)
Me.idleTicksToIgnore = TimeSpan.TicksPerSecond * idleSecondsToIgnore
StartTime = DateTime.Now
lastActiveTime = StartTime
Application.AddMessageFilter(Me)
AddHandler Application.ApplicationExit, Sub(sender As Object, e As EventArgs)
appIsRunning = False
UpdateTimeUsed()
Me._EndTime = DateAndTime.Now
End Sub
appIsRunning = True
End Sub
Public ReadOnly Property StartTime As DateTime
Public ReadOnly Property EndTime As DateTime
Public ReadOnly Property TotalRunTime As TimeSpan
Get
Return If(appIsRunning, DateTime.Now, EndTime) - StartTime
End Get
End Property
Public ReadOnly Property TimeUsed As TimeSpan
Get
Dim ticks As Int64 = timeUsedTicksAccumulator
If appIsRunning Then
Dim now As DateTime = DateTime.Now
Dim diff As TimeSpan = now - lastActiveTime
If diff.Ticks < idleTicksToIgnore Then
ticks += diff.Ticks
Else
' give usage credit for only the idle time threshold
ticks += idleTicksToIgnore
End If
End If
Return New TimeSpan(ticks)
End Get
End Property
Public Function PreFilterMessage(ByRef m As Message) As Boolean Implements IMessageFilter.PreFilterMessage
' Only messages posted to the thread message queue are received by this method.
' i.e. only messages that are processed by the message pump loop
' From: Message Routing, https://docs.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues#message-routing
' A message that is posted to a message queue is called a queued message.
' These are primarily the result of user input entered through the mouse or keyboard,
' such as WM_MOUSEMOVE, WM_LBUTTONDOWN, WM_KEYDOWN, and WM_CHAR messages.
' Other queued messages include the timer, paint, and quit messages: WM_TIMER,
' WM_PAINT, and WM_QUIT. Most other messages, which are sent directly to a
' window procedure, are called nonqueued messages.
Const WM_KEYFIRST As Int32 = &H100
Const WM_KEYLAST As Int32 = &H108
Const WM_MOUSEFIRST As Int32 = &H200
Const WM_MOUSELAST As Int32 = &H20E
Const WM_NCMOUSEMOVE As Int32 = &HA0
Const WM_NCLBUTTONDOWN As Int32 = &HA1
Const WM_NCLBUTTONUP As Int32 = &HA2
Const WM_NCLBUTTONDBLCLK As Int32 = &HA3
Const WM_NCRBUTTONDOWN As Int32 = &HA4
Const WM_NCRBUTTONUP As Int32 = &HA5
Const WM_NCRBUTTONDBLCLK As Int32 = &HA6
Const WM_NCMBUTTONDOWN As Int32 = &HA7
Const WM_NCMBUTTONUP As Int32 = &HA8
Const WM_NCMBUTTONDBLCLK As Int32 = &HA9
Const WM_NCMOUSEHOVER As Int32 = &H2A0
Const WM_NCMOUSELEAVE As Int32 = &H2A2
Select Case m.Msg
Case WM_KEYFIRST To WM_KEYLAST
UpdateTimeUsed()
Case WM_MOUSEFIRST To WM_MOUSELAST
UpdateTimeUsed()
Case WM_NCMOUSEMOVE, WM_NCLBUTTONDOWN, WM_NCLBUTTONUP, WM_NCLBUTTONDBLCLK, WM_NCRBUTTONDOWN, WM_NCRBUTTONUP, WM_NCRBUTTONDBLCLK, WM_NCMBUTTONDOWN, WM_NCMBUTTONUP, WM_NCMBUTTONDBLCLK
UpdateTimeUsed()
Case WM_NCMOUSEHOVER, WM_NCMOUSELEAVE
UpdateTimeUsed()
Case Else
'ignore it
End Select
Return False ' always return false as we are not handling the message per se
End Function
Private Sub UpdateTimeUsed()
Dim now As DateTime = DateTime.Now
Dim diff As TimeSpan = now - lastActiveTime
If diff.Ticks < idleTicksToIgnore Then
timeUsedTicksAccumulator += diff.Ticks
Else
' give usage credit for only the idle time threshold
timeUsedTicksAccumulator += idleTicksToIgnore
End If
lastActiveTime = now
End Sub
Public Overrides Function ToString() As String
Return $"Monitoring Started: {StartTime}{vbCrLf}Tot. Time: {Fmt(TotalRunTime)}{vbCrLf}Time Active: {Fmt(TimeUsed)}"
End Function
Private Shared Function Fmt(ts As TimeSpan) As String
Return $"{ts:dd} Days {ts:hh}:{ts:mm}:{ts:ss}.{ts:fff}"
End Function
End Class
创建此类实例的位置就在创建第一个表单并显示给用户之前。如果您使用的是 VB.Net 的应用程序框架,则可以将其添加到 MyApplication 类的定义中(可从打开 ApplicationEvents.vb 文件的项目菜单->项目属性->应用程序选项卡->查看应用程序事件按钮访问。
Imports Microsoft.VisualBasic.ApplicationServices
Namespace My
Partial Friend Class MyApplication
Public ReadOnly Property Usage As UsageMonitor = New UsageMonitor(600) ' 10 minute idle
Private Sub MyApplication_Shutdown(sender As Object, e As EventArgs) Handles Me.Shutdown
' add your logging code here
End Sub
End Class
End Namespace
在上面的代码中,实例是通过Usage 属性创建的。
如果您使用Sub Main 启动应用程序,请在调用Application.Run(mainform) 之前创建UsageMonitor 的实例。
要对此进行测试,请创建一个新的 WinForm 项目并在表单上放置一个标签(同时将上面显示的代码添加到项目中)。然后修改Form1.vb文件如下。
Public Class Form1
Private WithEvents Timer1 As Timer
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Timer1 = New Timer
Timer1.Interval = 2000
Timer1.Start()
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
Label1.Text = My.Application.Usage.ToString()
End Sub
End Class
请注意,您可能希望将 idleSecondsToIgnore 的值从 600 减少到 10 以进行测试,以便您可以观察到当您停止用户交互(将鼠标移到表单上)时活动时间值停止增加。