在 WPF 应用程序中实现基于用户不活动的自动注销






4.92/5 (36投票s)
如何在 WPF 应用程序中检查用户不活动
引言
在某些应用程序中,出于安全原因,我们需要实现自动注销功能。在自动注销功能中,如果用户在一定时间内不活动,应用程序会将用户重定向到登录窗口。我写这篇文章是为了描述如何在WPF应用程序中基于用户不活动实现自动注销功能。在Web应用程序中,如果用户在设定的时间内不活动(由应用程序设置),会话会超时并重定向到登录页面。然而,桌面应用程序并非如此。在桌面应用程序中,您必须显式实现注销功能。有许多方法可以在WPF应用程序中实现注销功能,我将在本文中进行描述。从我的角度来看,使用钩子技术实现自动注销功能在性能方面是最好的方法。这里我描述了使用钩子方法实现自动注销,并提供了源代码。这是一个简单的任务,但我在此发布它,因为我相信它可以节省您宝贵的时间。
实现基本思路
这里实现的自动注销功能如下:当操作员在自动注销时间内不活动时,系统会自动注销该操作员。此功能已实现滑动效果,即当发生鼠标点击和键盘输入等操作员活动时,计时器会重新启动。实现的方法如下:
- 启动一个计时器,其间隔等于用户注销时间,并在该计时器的滴答事件中,将操作员重定向到登录窗口。
- 每个窗口都通过钩子技术监听操作系统的消息(如果按窗口为基础进行钩子,Windows操作系统只会将特定于该窗口的消息发送给该窗口)。
- 如果收到操作系统消息,则检查该操作系统消息是否为用户活动。如果消息是用户活动,则重置计时器(它将重新开始计时)。否则,不执行任何操作。
应用程序如何监听操作系统(OS)消息?
应用程序可以通过钩子监听操作系统消息。根据MSDN,`hook`是系统消息处理机制中的一个点,应用程序可以在此安装一个子程序来监视系统中的消息流量,并在某些类型的消息到达目标窗口过程之前对其进行处理。Windows钩子使用回调函数来实现。钩子的例子:在键盘或鼠标事件消息到达应用程序之前拦截它们。Windows OS支持许多不同类型的钩子;每种类型都可以访问其消息处理机制的不同方面。例如,应用程序可以使用`WH_MOUSE`钩子来监视鼠标消息的消息流量。在这里,每个窗口都使用了钩子来监视特定窗口的消息流量。
应用程序如何判断操作系统消息是否为用户活动?
每个操作系统消息都对应一个常量整数值,表示消息的类型。例如,`MOUSEHOVER`的操作系统消息值为`0x02A1`(`WM_MOUSEHOVER = 0x02A1`)。由于每个窗口消息都有预定义的整数值,应用程序会维护一个整数值列表,这些整数值代表用户活动类型的操作系统消息。收到操作系统消息时,应用程序会检查收到的操作系统消息整数值与列表整数值之间是否存在匹配。每个消息的整数值(十六进制)的完整列表如下。
WM_NULL = 0x0000, WM_CREATE = 0x0001, WM_DESTROY = 0x0002
WM_MOVE = 0x0003, WM_SIZE = 0x0005, WM_ACTIVATE = 0x0006
WM_SETFOCUS = 0x0007, WM_KILLFOCUS = 0x0008, WM_ENABLE = 0x000A
WM_SETREDRAW = 0x000B, WM_SETTEXT = 0x000C, WM_GETTEXT = 0x000D
WM_GETTEXTLENGTH = 0x000E, WM_PAINT = 0x000F, WM_CLOSE = 0x0010
WM_QUERYENDSESSION = 0x0011, WM_QUIT = 0x0012, WM_QUERYOPEN = 0x0013
WM_ERASEBKGND = 0x0014, WM_SYSCOLORCHANGE = 0x0015, WM_ENDSESSION = 0x0016
WM_SHOWWINDOW = 0x0018, WM_CTLCOLOR = 0x0019, WM_WININICHANGE = 0x001A
WM_SETTINGCHANGE = 0x001A, WM_DEVMODECHANGE = 0x001B, WM_ACTIVATEAPP = 0x001C
WM_FONTCHANGE = 0x001D, WM_TIMECHANGE = 0x001E, WM_CANCELMODE = 0x001F
WM_SETCURSOR = 0x0020, WM_MOUSEACTIVATE = 0x0021, WM_CHILDACTIVATE = 0x0022
WM_QUEUESYNC = 0x0023, WM_GETMINMAXINFO = 0x0024, WM_PAINTICON = 0x0026
WM_ICONERASEBKGND = 0x0027, WM_NEXTDLGCTL = 0x0028, WM_SPOOLERSTATUS = 0x002A
WM_DRAWITEM = 0x002B, WM_MEASUREITEM = 0x002C, WM_DELETEITEM = 0x002D
WM_VKEYTOITEM = 0x002E, WM_CHARTOITEM = 0x002F, WM_SETFONT = 0x0030
WM_GETFONT = 0x0031, WM_SETHOTKEY = 0x0032, WM_GETHOTKEY = 0x0033
WM_QUERYDRAGICON = 0x0037, WM_COMPAREITEM = 0x0039, WM_GETOBJECT = 0x003D
WM_COMPACTING = 0x0041, WM_COMMNOTIFY = 0x0044 , WM_WINDOWPOSCHANGING = 0x0046
WM_WINDOWPOSCHANGED = 0x0047, WM_POWER = 0x0048, WM_COPYDATA = 0x004A
WM_CANCELJOURNAL = 0x004B, WM_NOTIFY = 0x004E, WM_INPUTLANGCHANGEREQUEST = 0x0050
WM_INPUTLANGCHANGE = 0x0051, WM_TCARD = 0x0052, WM_HELP = 0x0053
WM_USERCHANGED = 0x0054, WM_NOTIFYFORMAT = 0x0055, WM_CONTEXTMENU = 0x007B
WM_STYLECHANGING = 0x007C, WM_STYLECHANGED = 0x007D, WM_DISPLAYCHANGE = 0x007E
WM_GETICON = 0x007F, WM_SETICON = 0x0080, WM_NCCREATE = 0x0081
WM_NCDESTROY = 0x0082, WM_NCCALCSIZE = 0x0083, WM_NCHITTEST = 0x0084
WM_NCPAINT = 0x0085, WM_NCACTIVATE = 0x0086, WM_GETDLGCODE = 0x0087
WM_SYNCPAINT = 0x0088, WM_NCMOUSEMOVE = 0x00A0, WM_NCLBUTTONDOWN = 0x00A1
WM_NCLBUTTONUP = 0x00A2, WM_NCLBUTTONDBLCLK = 0x00A3, WM_NCRBUTTONDOWN = 0x00A4
WM_NCRBUTTONUP = 0x00A5, WM_NCRBUTTONDBLCLK = 0x00A6, WM_NCMBUTTONDOWN = 0x00A7
WM_NCMBUTTONUP = 0x00A8, WM_NCMBUTTONDBLCLK = 0x00A9, WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101, WM_CHAR = 0x0102, WM_DEADCHAR = 0x0103
WM_SYSKEYDOWN = 0x0104, WM_SYSKEYUP = 0x0105, WM_SYSCHAR = 0x0106
WM_SYSDEADCHAR = 0x0107, WM_KEYLAST = 0x0108, WM_IME_STARTCOMPOSITION = 0x010D
WM_IME_ENDCOMPOSITION = 0x010E, WM_IME_COMPOSITION = 0x010F, WM_IME_KEYLAST = 0x010F
WM_INITDIALOG = 0x0110, WM_COMMAND = 0x0111, WM_SYSCOMMAND = 0x0112
WM_TIMER = 0x0113, WM_HSCROLL = 0x0114, WM_VSCROLL = 0x0115
WM_INITMENU = 0x0116, WM_INITMENUPOPUP = 0x0117, WM_MENUSELECT = 0x011F
WM_MENUCHAR = 0x0120, WM_ENTERIDLE = 0x0121, WM_MENURBUTTONUP = 0x0122
WM_MENUDRAG = 0x0123, WM_MENUGETOBJECT = 0x0124, WM_UNINITMENUPOPUP = 0x0125
WM_MENUCOMMAND = 0x0126, WM_CTLCOLORMSGBOX = 0x0132, WM_CTLCOLOREDIT = 0x0133
WM_CTLCOLORLISTBOX = 0x0134, WM_CTLCOLORBTN = 0x0135, WM_CTLCOLORDLG = 0x0136
WM_CTLCOLORSCROLLBAR = 0x0137, WM_CTLCOLORSTATIC = 0x0138, WM_MOUSEMOVE = 0x0200
WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202, WM_LBUTTONDBLCLK = 0x0203
WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205, WM_RBUTTONDBLCLK = 0x0206
WM_MBUTTONDOWN = 0x0207, WM_MBUTTONUP = 0x0208, WM_MBUTTONDBLCLK = 0x0209
WM_MOUSEWHEEL = 0x020A, WM_PARENTNOTIFY = 0x0210, WM_ENTERMENULOOP = 0x0211
WM_EXITMENULOOP = 0x0212, WM_NEXTMENU = 0x0213, WM_SIZING = 0x0214
WM_CAPTURECHANGED = 0x0215, WM_MOVING = 0x0216, WM_DEVICECHANGE = 0x0219
WM_MDICREATE = 0x0220, WM_MDIDESTROY = 0x0221, WM_MDIACTIVATE = 0x0222
WM_MDIRESTORE = 0x0223, WM_MDINEXT = 0x0224, WM_MDIMAXIMIZE = 0x0225
WM_MDITILE = 0x0226, WM_MDICASCADE = 0x0227, WM_MDIICONARRANGE = 0x0228
WM_MDIGETACTIVE = 0x0229, WM_MDISETMENU = 0x0230, WM_ENTERSIZEMOVE = 0x0231
WM_EXITSIZEMOVE = 0x0232, WM_DROPFILES = 0x0233, WM_MDIREFRESHMENU = 0x0234
WM_IME_SETCONTEXT = 0x0281, WM_IME_NOTIFY = 0x0282, WM_IME_CONTROL = 0x0283
WM_IME_COMPOSITIONFULL = 0x0284, WM_IME_SELECT = 0x0285, WM_IME_CHAR = 0x0286
WM_IME_REQUEST = 0x0288, WM_IME_KEYDOWN = 0x0290, WM_IME_KEYUP = 0x0291
WM_MOUSEHOVER = 0x02A1, WM_MOUSELEAVE = 0x02A3, WM_CUT = 0x0300
WM_COPY = 0x0301, WM_PASTE = 0x0302, WM_CLEAR = 0x0303
WM_UNDO = 0x0304, WM_RENDERFORMAT = 0x0305, WM_RENDERALLFORMATS = 0x0306
WM_DESTROYCLIPBOARD = 0x0307, WM_DRAWCLIPBOARD = 0x0308, WM_PAINTCLIPBOARD = 0x0309
WM_VSCROLLCLIPBOARD = 0x030A, WM_SIZECLIPBOARD = 0x030B, WM_ASKCBFORMATNAME = 0x030C
WM_CHANGECBCHAIN = 0x030D, WM_HSCROLLCLIPBOARD = 0x030E, WM_QUERYNEWPALETTE = 0x030F
WM_PALETTEISCHANGING = 0x0310, WM_PALETTECHANGED = 0x0311, WM_HOTKEY = 0x0312
WM_PRINT = 0x0317, WM_PRINTCLIENT = 0x0318, WM_HANDHELDFIRST = 0x0358
WM_HANDHELDLAST = 0x035F, WM_AFXFIRST = 0x0360, WM_AFXLAST = 0x037F
WM_PENWINFIRST = 0x0380, WM_PENWINLAST = 0x038F, WM_APP = 0x8000
WM_USER = 0x0400, WM_REFLECT = WM_USER + 0x1c00
为什么使用System.Windows.Forms/System.Timers.Timer而不是DispatcherTimer?
`System.Windows.Forms timer`/`System.Timers.timer`运行在与用户界面(UI)线程不同的线程上,而`DispatcherTimer`运行在用户界面(UI)线程上。因此,`DispatcherTimer`不依赖于UI更新,但`System.Windows.Forms` timer则不然。根据MSDN,`DispatcherTimer`是一个集成到调度程序队列中的计时器,该队列以指定的间隔和指定的优先级进行处理。`Dispatcher timers`不保证在时间间隔发生时执行,但它们保证不会在时间间隔发生之前执行。这是因为`DispatcherTimer`操作像其他UI操作一样被放入调度程序队列中。`DispatcherTimer`滴答事件的执行依赖于调度程序队列中的其他UI作业及其优先级。`System.Windows.Forms/System.Timers.timer`保证在时间间隔发生时精确执行。
为什么System.Windows.Threading.Dispatcher.CurrentDispatcher.
Hooks.OperationPosted事件对我不起作用?
看起来`System.Windows.Threading.Dispatcher.CurrentDispatcher.
Hooks.OperationPostedevent`可以用于在WPF应用程序中实现注销功能。当有内容被添加到WPF`dispatcher queue`时,此事件会被触发。当发生任何用户活动时,工作项会被发布到`dispatcher queue`;同样,当任何后台处理的结果试图更新UI(如窗口中的时钟)时,工作项也会被发布到`dispatcher queue`。当应用程序从网络获取数据并尝试更新UI时,情况也是如此。所以底线是,这个事件不仅在用户活动时触发,也在代码更新UI时触发。因此,当应用程序有更新UI的后台工作时,检测`OperationPosted`是无效的。如果应用程序没有更新UI的后台工作,那么`OperationPosted`事件将适用于该应用程序实现自动注销功能。
为什么不使用GetLastInputInfo函数来确定用户不活动?
使用`GetLastInputInfo`函数,可以实现自动注销功能,因为它检索最后输入事件的时间。然而,您必须定期调用此函数来检测用户是否在一段时间内处于空闲状态。这样,应用程序就必须使用轮询策略来实现自动注销,并且应用程序会不必要地占用CPU。因此,如果您不关心CPU使用率(性能),可以选择这种方式。
如果您的应用程序有多个窗口,您会怎么做?
我们知道,应用程序正在监听特定于窗口的操作系统消息,操作系统不会将一个窗口的消息发送给另一个窗口。因此,在每个窗口中,应用程序都必须监听操作系统消息。所以应用程序必须在每个窗口中钩取窗口消息,当收到用户活动类型的消息时,应用程序必须重置计时器。因此,您必须在每个窗口中编写一些重复的代码。
代码说明
实现了一个名为`AutoLogOffHelper`的`Autologoff`计时器类,以根据`autologoff`功能提供计时器功能。`AutoLogOffHelper`类公开了启动自动注销计时器和重置自动注销时间的功能。要启动自动注销计时器,必须调用`AutoLogOffHelper`类的`StartAutoLogoffOption()`方法。当线程空闲时,会触发`System.Windows.Interop.ComponentDispatcher.ThreadIdle`。这里的线程空闲意味着`dispatcher queue`中没有消息。在这里,应用程序在`Thread Idle`事件处理程序中启动计时器。在`StartAutoLogoffOption()`方法中,`System.Windows.Interop.ComponentDispatcher.ThreadIdle`事件被设置为名为`DispatcherQueueEmptyHandler`的事件处理程序。`AutoLogOffHelper`类的使用代码如下:
class AutoLogOffHelper
{
static System.Windows.Forms.Timer _timer = null;
static private int _logOffTime;
static public int LogOffTime
{
get { return _logOffTime; }
set { _logOffTime = value; }
}
public delegate void MakeAutoLogOff();
static public event MakeAutoLogOff MakeAutoLogOffEvent;
static public void StartAutoLogoffOption()
{
System.Windows.Interop.ComponentDispatcher.ThreadIdle += new
EventHandler(DispatcherQueueEmptyHandler);
}
static void _timer_Tick(object sender, EventArgs e)
{
if (_timer != null)
{
System.Windows.Interop.ComponentDispatcher.ThreadIdle -= new
EventHandler(DispatcherQueueEmptyHandler);
_timer.Stop();
_timer = null;
if (MakeAutoLogOffEvent != null)
{
MakeAutoLogOffEvent();
}
}
}
static void DispatcherQueueEmptyHandler(object sender, EventArgs e)
{
if (_timer == null)
{
_timer = new System.Windows.Forms.Timer();
_timer.Interval = LogOffTime * 60 * 1000;
_timer.Tick += new EventHandler(_timer_Tick);
_timer.Enabled = true;
}
else if (_timer.Enabled == false)
{
_timer.Enabled = true;
}
}
static public void ResetLogoffTimer()
{
if (_timer != null)
{
_timer.Enabled = false;
_timer.Enabled = true;
}
}
}
此类公开了一个名为`LogOffTime`的属性,应用程序可以通过该属性设置注销时间。在`DispatcherQueueEmptyHandler`中,使用`LogOffTime`时间设置间隔来启动计时器。计时器类的滴答事件被设置为名为`_timer_Tick`的事件处理程序。当计时器的滴答事件被触发时,意味着在没有任何用户活动的情况下,注销时间已过,用户将被重定向到登录窗口。因此,在`_timer_Tick`方法中,会触发`MakeAutoLogOffEvent`以通知应用程序窗口将操作员重定向到登录窗口。`ResetLogoffTimer`方法暴露出来,以便在发生用户活动时重置计时器。
要跟踪窗口上的用户活动,将检索窗口的Win32处理程序。`HwndSource.FromHwnd`方法返回一个`HwndSource`,其中`HwndSource`代表Win32窗口内的WPF内容。然后使用`AddHook
`方法添加一个名为`CallBackMethod`的回调方法,该方法将接收窗口的所有消息。为此,使用了以下代码:
HwndSource windowSpecificOSMessageListener = HwndSource.FromHwnd(new
WindowInteropHelper(this).Handle);
windowSpecificOSMessageListener.AddHook(new HwndSourceHook(CallBackMethod));
然后,设置`AutoLogOffHelper`类的`LogOffTime`属性,并将`AutoLogOffHelper`类的`MakeAutoLogOffEvent`事件设置为名为`AutoLogOffHelper_MakeAutoLogOffEvent`的事件处理程序,该处理程序将用户重定向到登录窗口。然后调用`StartAutoLogoffOption`方法启动自动注销计时器。
AutoLogOffHelper.LogOffTime = logOffTime;
AutoLogOffHelper.MakeAutoLogOffEvent +=
new AutoLogOffHelper.MakeAutoLogOff(AutoLogOffHelper_MakeAutoLogOffEvent);
AutoLogOffHelper.StartAutoLogoffOption();
在`Callback Method`中,接收该窗口特定的所有操作系统消息。然后测试操作系统消息,以确定消息是否为用户活动。例如,使用0x0021来测试窗口消息是否为`MOUSEACTIVATE`。如果消息是用户活动,则调用`AutoLogOffHelper.ResetLogoffTimer()`来重置计时器。为此,使用了以下代码:
private IntPtr CallBackMethod(IntPtr hwnd,
int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
// Testing OS message to determine whether it is a user activity or not
if ((msg >= 0x0200 && msg <= 0x020A) || (msg <= 0x0106 && msg >= 0x00A0) ||
msg == 0x0021)
{
AutoLogOffHelper.ResetLogoffTimer();
string time = DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss tt");
tblStatus.Text = "Timer is reset on user activity at " + ": " + time;
tblLastUserActivityTypeOSMessage.Text =
"Last user activity type OS message the application considers " +
": 0x" + msg.ToString("X");
}
else
{
tblLastOSMessage.Text = "Last OS Message " + ": 0x" + msg.ToString("X");
// For debugging purpose
// If this auto logoff does not work for some user activity, you
// can detect the integer code of that activity using the following line.
// Then All you need to do is add this integer code to the above if condition.
System.Diagnostics.Debug.WriteLine(msg.ToString());
}
return IntPtr.Zero;
}
结论
感谢阅读本文。尽管这是一项非常简单的工作,但我在此发布它,因为我认为很多人像我一样需要这个功能。希望这能节省您一些时间。如果您有任何问题,我很乐意回答。我一直很欣赏评论。
历史
- 首次发布 – 2009年10月9日