首页 > 解决方案 > 我如何确定任何可能的申请表是否有效?

问题描述

我收到了一个客户的特殊请求,我为其制作了一个 vb.net 桌面应用程序。他需要记录使用该应用程序所花费的时间。使用“应用程序”是指它的任何形式(模态或其他形式)都是活动的,并且应用程序不仅仅是在后台运行或最小化。有没有办法判断属于该应用程序的任何表单是否处于活动状态?一旦我弄清楚这一点,我想我可以找到一种记录“活动”时间的方法。

标签: vb.netwinforms

解决方案


可以通过使用实现IMessageFilter 接口的类并使用Application.AddMessageFilter(IMessageFilter) 方法安装此类的实例来监视 WinForm 项目中的键盘和鼠标活动。请注意,这种方式只能检测到发布到线程消息队列的消息;幸运的是,所有键盘和鼠标消息都发布到此队列。(有关详细信息,请参阅:关于消息和消息队列

以下是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属性创建的。

如果您使用 aSub Main来启动应用程序,请UsageMonitor在调用之前创建一个实例Application.Run(mainform)

要对此进行测试,请创建一个新的 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 以进行测试,以便您可以观察到当您停止用户交互(将鼠标移到表单上)时活动时间值停止增加。


推荐阅读