这是DoEvents的正确使用吗? - 包括示例项目



我正在使用第一个代码块在单独的线程上执行低级鼠标钩子。它实际上是这样工作的(信不信由你(,因为订阅的行为初始化了钩子。使用钩子,我需要能够阻止调用事件的方法,以便我可以设置一个值来更改其执行过程。这就是为什么我不能简单地将事件处理程序卸载到另一个线程的原因。

我的问题是,即使这有效,还有另一种方法可以避免DoEvents吗?

DoEvents是否可能仅适用于其自身线程上的事件,或者此调用是否会影响我的 GUI 线程?据我所知,它似乎根本不影响我的 GUI。

注意:如果不调用Sleep,CPU 将显著增加。
注意:如果不DoEvents挂机消息会累积并强制操作系统断开挂机。

编辑:我创建了一个示例项目,以便你们可以对此进行测试。该应用程序将在单独的线程上启动鼠标挂钩,并捕获鼠标右键单击,并通过消息框通知您它这样做了。您可以使用以下链接获取该项目。

该示例显示您可以阻止 GUI 线程,并且仍然可以毫无问题地处理挂钩,从而确认挂钩在其自己的线程上。

https://github.com/mzomparelli/Threaded-Low-Level-Mouse-Hook-Example

我现在开始认为这是对DoEvents的有效使用,尽管许多人声称DoEvents总是不好的。

private static bool blnStopMouseHook = false;
public static void StartMouseHook()
{
if (MouseHook == null)
{
blnStopMouseHook = false;
MouseHook = new Thread(new ThreadStart(() => { MouseHookThread(); }));
MouseHook.SetApartmentState(ApartmentState.STA);
MouseHook.Start();
}
}
public static void StopMouseHook()
{
blnStopMouseHook = true;
MouseHook.Join();
MouseHook = null;
}
private static void MouseHookThread()
{
HookManager.MouseWheel += HookHandlers.HookManagerOnMouseWheel;
HookManager.MouseClickExt += HookHandlers.HookManagerOnMouseClickExt;
do
{
System.Threading.Thread.Sleep(1);
Application.DoEvents();
} while (blnStopMouseHook == false);
HookManager.MouseWheel -= HookHandlers.HookManagerOnMouseWheel;
HookManager.MouseClickExt -= HookHandlers.HookManagerOnMouseClickExt;
}

下面是我的 HookProc 的一个片段,它创建了事件HookManagerOnMouseWheel

private static int MouseHookProc(int nCode, int wParam, IntPtr lParam)
{
if (nCode >= 0)
{
//Marshall the data from callback.
MouseLLHookStruct mouseHookStruct = (MouseLLHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLLHookStruct));
switch (wParam)
{
case WM_MOUSEWHEEL:
mouseDelta = (short)((mouseHookStruct.MouseData >> 16) & 0xffff);
break;
}
//generate event 
MouseEventExtArgs e = new MouseEventExtArgs(
button,
clickCount,
mouseHookStruct.Point.X,
mouseHookStruct.Point.Y,
mouseDelta);

//Wheel was moved
if (s_MouseWheel!=null && mouseDelta!=0)
{
s_MouseWheel.Invoke(null, e);
}

//If someone listens to move and there was a change in coordinates raise move event
if (e.Handled)
{
return -1;
}
}
//call next hook
return CallNextHookEx(s_MouseHookHandle, nCode, wParam, lParam);
}

这是我的事件处理程序。

public static void HookManagerOnMouseWheel(object sender, MouseEventExtArgs mouseEventArgs)
{
int iHotkey;
int iHotkey2;
string keyCombination = CurrentModifiers();
string keyCombination2 = CurrentModifiers();
if (Window.Taskbar().IsMouseOver() || Window.Taskbar2().IsMouseOver())
{
//Create combination string
if (mouseEventArgs.Delta < 0)
{
keyCombination = keyCombination + "+MOUSE-TASKBAR-SCROLL-DOWN";
keyCombination2 = keyCombination2 + "+MOUSE-ANYWHERE-SCROLL-DOWN";
}
else
{
keyCombination = keyCombination + "+MOUSE-TASKBAR-SCROLL-UP";
keyCombination2 = keyCombination2 + "+MOUSE-ANYWHERE-SCROLL-UP";
}
iHotkey = GLOBALS.hotkeys.FindIndex(l => l.HotkeyString() == keyCombination);
iHotkey2 = GLOBALS.hotkeys.FindIndex(l => l.HotkeyString() == keyCombination2);
if (iHotkey >= 0)
{
ExecuteAction(iHotkey);
mouseEventArgs.Handled = true;
return;
}
else if (iHotkey2 >= 0)
{
ExecuteAction(iHotkey2);
mouseEventArgs.Handled = true;
return;
}
}
if (mouseEventArgs.Delta < 0)
{
keyCombination = keyCombination + "+MOUSE-ANYWHERE-SCROLL-DOWN";
}
else
{
keyCombination = keyCombination + "+MOUSE-ANYWHERE-SCROLL-UP";
}
iHotkey = GLOBALS.hotkeys.FindIndex(l => l.HotkeyString() == keyCombination);
if (iHotkey >= 0)
{
ExecuteAction(iHotkey);
mouseEventArgs.Handled = true;
return;
}

}

多线程的问题在于,不能保证主线程不会发送超过工作线程可以处理的事件,或者工作线程会因为缺少任何操作而"饥饿"。这就是为什么你的代码有那些丑陋的SleepDoEvents调用。

您需要的是一种同步机制。

在这种情况下,我建议你遵循生产者-消费者模式,这需要一个队列。在这种情况下,我建议您为队列使用 BlockingCollection。

阻塞集合将允许主线程向其添加事件,并提供允许工作线程从中获取事件的方法。如果没有事件,集合将阻止工作线程,直到有可用的工作线程。

因此,首先,声明一个用于保存事件的数据结构:

struct Event 
{
object sender;
EventArgs e;
}

然后声明您的队列:

private BlockingCollection<Event> _queue = new BlockingCollection<Event>();

在主线程中,使用添加到队列的普通事件处理程序处理事件:

private OnMouseAction(object sender, EventArgs e)
{
_queue.Add(new Event {sender = sender, e = e});
}

在工作线程中,只需读取队列并对其执行操作:

private void MouseHookWorker(CancellationToken token)
{
try
{
while (!token.IsCancellationRequested)
{
var event = _queue.Take(token);
ProcessEvent(event.sender, event.e);
}
}
catch (OperationCanceledException ex)
{
}
}     

并在ProcessEvent中实施实际工作(无论那是什么(。

若要停止工作线程,可以发出取消令牌的信号,也可以使用_queue.CompleteAdding();停止队列

CancelToken是可选的,但可能是一个好主意。如果您不知道如何使用它,请参阅此问题。

使用现有代码,您将事件附加到新创建的线程上,但这并不意味着新线程将处理这些事件。事实上并非如此。导致引发事件的任何线程将继续处理该事件。这就是代码中需要DoEvents调用的原因。

您需要一种方法在引发事件后将事件封送到后台线程。

如果我是你,我会为此使用Microsoft的反应式框架(Rx(。只需NuGet "System.Reactive"用于主要位,"System.Reactive.Windows.Forms"用于WinForms位,"System.Reactive.Windows.Threading"用于WPF位。

然后你可以做这样的事情:

IObservable<EventPattern<MouseEventArgs>> mouseMoves =
Observable
.FromEventPattern<MouseEventHandler, MouseEventArgs>(
h => this.MouseMove += h,
h => this.MouseMove -= h);
IDisposable subscription =
mouseMoves
.ObserveOn(Scheduler.Default)
.Do(ep =>
{
Console.WriteLine("Th" + Thread.CurrentThread.ManagedThreadId);
})
.ObserveOn(this)
.Subscribe(ep =>
{
Console.WriteLine("UI" + Thread.CurrentThread.ManagedThreadId);
});
Console.WriteLine("!" + Thread.CurrentThread.ManagedThreadId);

此代码处理事件,但通过.ObserveOn(Scheduler.Default)调用将事件推送到后台线程。

在本例中,我使用了 Windows 窗体,因此.ObserveOn(this)(this是当前窗体(将调用推送回 UI 线程。

因此,当我运行此代码并在窗体上移动鼠标时,我得到如下输出:

!1 Th4 Th4 提示 提示 Th4 提示 Th4 提示 Th4 提示 Th4 UI1

应该相当清楚地看到,此代码正确地将调用推送到后台线程,然后推送回 UI。

不会发生阻塞。

要分离事件处理程序,只需调用subscription.Dispose();

在我的代码中,我安装了NuGet包"System.Reactive"和"System.Reactive.Windows.Forms",并添加了以下using

using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Concurrency;
using System.Threading;

试试这个代码:

IDisposable subscription =
(
from mm in mouseMoves
let direction = mm.EventArgs.Delta < 0 ? "DOWN" : "UP"
let keyCombination = CurrentModifiers() + "+MOUSE-ANYWHERE-SCROLL-" + direction
let iHotkey = GLOBALS.hotkeys.FindIndex(l => l.HotkeyString() == keyCombination)
where iHotkey >= 0
select new { mm.EventArgs, iHotkey }
)
.Do(x => x.EventArgs.Handled = true)
.ObserveOn(Scheduler.Default)
.Subscribe(x => ExecuteAction(x.iHotkey));

这将阻止传入调用方并尽快设置EventArgs.Handled = true,然后再将调用推送到其他线程上的ExecuteAction(x.iHotkey)

最新更新