Vanara/PInvoke/User32/SystemEventHandler.cs

296 lines
9.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using Vanara.InteropServices;
using Vanara.PInvoke;
using static Vanara.PInvoke.Kernel32;
using static Vanara.PInvoke.User32;
namespace Vanara.PInvoke
{
/// <summary>
/// An event handler that is dependent on window messages. This class works on both windowed and console applications, creating a
/// threaded message pump if needed.
/// <para>
/// To use, derive a class and override the <see cref="MessageFilter"/> method. When handling the message, use the <see
/// cref="RaiseEvent(Guid, object[])"/> method to call the event.
/// </para>
/// <para>
/// Delegates can be registered and unregistered using unique GUID values via the <see cref="AddEvent(Guid, Delegate)"/> and <see
/// cref="RemoveEvent(Guid, Delegate)"/> methods.
/// </para>
/// </summary>
/// <seealso cref="System.IDisposable"/>
public abstract class SystemEventHandler : IDisposable
{
private static ManualResetEvent eventWindowReady;
private static Thread windowThread;
private readonly Dictionary<Guid, Delegate> eventHandles = new Dictionary<Guid, Delegate>();
private readonly object lockObj = new object();
private bool disposedValue;
private BasicMessageWindow msgWindow;
/// <summary>Initializes a new instance of the <see cref="SystemEventHandler"/> class.</summary>
/// <param name="forceThread">
/// if set to <see langword="true"/> a new thread is created for the message pump regardless of the current apartment state.
/// </param>
/// <exception cref="System.InvalidOperationException">System events not supported.</exception>
protected SystemEventHandler(bool forceThread = false)
{
if (Thread.GetDomain().GetData(".appDomain") != null)
throw new InvalidOperationException("System events not supported.");
if (!forceThread && !UserSession.IsInteractive || Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
{
Init();
}
else
{
ThreadRunning = new ManualResetEvent(false);
eventWindowReady = new ManualResetEvent(false);
windowThread = new Thread(MTAThreadProc) { IsBackground = true, Name = typeof(SystemEventHandler).FullName };
windowThread.Start(this);
eventWindowReady.WaitOne();
}
}
/// <summary>Finalizes an instance of the <see cref="SystemEventHandler"/> class.</summary>
~SystemEventHandler()
{
Dispose(false);
}
/// <summary>Occurs when the message window handle has been created.</summary>
public event EventHandler MessageWindowHandleCreated;
/// <summary>Gets a value indicating whether this instance is running in a thread.</summary>
/// <value><see langword="true"/> if this instance is running in a thread; otherwise, <see langword="false"/>.</value>
public bool IsRunningInThread => ThreadRunning is not null;
/// <summary>Gets the message window handle which can be used to register for messaged events.</summary>
/// <value>The message window handle.</value>
public HWND MessageWindowHandle => msgWindow?.Handle ?? HWND.NULL;
private ManualResetEvent ThreadRunning { get; set; }
/// <summary>Adds a delegate and its associated key to the handler list.</summary>
/// <param name="key">The key.</param>
/// <param name="value">The delegate value.</param>
public void AddEvent(Guid key, Delegate value)
{
lock (lockObj)
{
if (!eventHandles.TryGetValue(key, out Delegate h))
{
eventHandles.Add(key, value);
OnEventAdd(key);
}
else
{
eventHandles[key] = Delegate.Combine(h, value);
}
}
}
/// <summary>Removes a delegate and its associated key to the handler list.</summary>
/// <param name="key">The key.</param>
/// <param name="value">The delegate value.</param>
public void RemoveEvent(Guid key, Delegate value)
{
lock (lockObj)
{
if (eventHandles.TryGetValue(key, out Delegate h))
{
h = Delegate.Remove(h, value);
if (h is null || h.GetInvocationList().Length == 0)
{
eventHandles.Remove(key);
OnEventRemove(key);
}
else
{
eventHandles[key] = h;
}
}
else
{
OnEventRemove(key);
}
}
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
void IDisposable.Dispose()
{
Dispose(true);
System.GC.SuppressFinalize(this);
}
/// <summary>Releases unmanaged and - optionally - managed resources.</summary>
/// <param name="disposing">
/// <see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if (ThreadRunning != null && IsWindow(MessageWindowHandle))
{
PostMessage(MessageWindowHandle, (uint)WindowMessage.WM_QUIT);
ThreadRunning.WaitOne(5000);
ThreadRunning = null;
}
}
msgWindow.Dispose();
disposedValue = true;
}
}
/// <summary>Determines whether the specified key has a delegate handler in its list.</summary>
/// <param name="key">The key.</param>
/// <returns><see langword="true"/> if the specified key exists; otherwise, <see langword="false"/>.</returns>
protected bool HasKey(Guid key)
{
lock (lockObj)
return eventHandles.ContainsKey(key);
}
/// <summary>Provides access to the WndProc listening for messages.</summary>
/// <param name="hwnd">A handle to the window procedure that received the message.</param>
/// <param name="msg">The message.</param>
/// <param name="wParam">Additional message information. The content of this parameter depends on the value of the Msg parameter.</param>
/// <param name="lParam">Additional message information. The content of this parameter depends on the value of the Msg parameter.</param>
/// <param name="lReturn">The return value is the result of the message processing and depends on the message.</param>
/// <returns>
/// <see langword="true"/> if this message should be considered handled; or <see langword="false"/> to pass the message along to
/// <see cref="DefWindowProc"/>.
/// </returns>
protected abstract bool MessageFilter(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam, out IntPtr lReturn);
/// <summary>
/// Allows derived classes to pre-process a message in a threaded message pump so that it is not passed along. Useful for things
/// like <see cref="User32.IsDialogMessage"/>.
/// </summary>
/// <param name="msg">The MSG.</param>
/// <returns>
/// <see langword="true"/> if the message was processed and should not be translated and dispatched; <see langword="false"/>
/// otherwise. <see langword="false"/> is the default.
/// </returns>
protected virtual bool PreprocessMessage(in MSG msg) => false;
/// <summary>Called when an event has been added.</summary>
/// <param name="key">The event key.</param>
protected virtual void OnEventAdd(Guid key)
{
}
/// <summary>Called when an event has been removed.</summary>
/// <param name="key">The event key.</param>
protected virtual void OnEventRemove(Guid key)
{
}
/// <summary>Called when the message window handle is created.</summary>
protected virtual void OnMessageWindowHandleCreated() => MessageWindowHandleCreated?.Invoke(this, EventArgs.Empty);
/// <summary>Calls the delegate list associated with the key, passing the supplied parameters.</summary>
/// <param name="key">The key.</param>
/// <param name="args">The arguments.</param>
/// <returns>The value returned by the call to the delegate list.</returns>
/// <exception cref="InvalidOperationException">Event for {key} is not registered.</exception>
protected object RaiseEvent(Guid key, params object[] args)
{
Delegate h;
lock (lockObj)
{
if (!eventHandles.TryGetValue(key, out h))
throw new InvalidOperationException($"Event for {key} is not registered.");
}
return h.DynamicInvoke(args);
}
private static void MTAThreadProc(object param)
{
var handler = (SystemEventHandler)param;
try
{
handler.Init();
eventWindowReady.Set();
if (!handler.MessageWindowHandle.IsNull)
{
var keepRunning = true;
while (keepRunning)
{
var ret = MsgWaitForMultipleObjectsEx(0, null, 100, QS.QS_ALLINPUT, MWMO.MWMO_INPUTAVAILABLE);
if (ret == (uint)WAIT_STATUS.WAIT_TIMEOUT)
{
Thread.Sleep(1);
}
else
{
while (PeekMessage(out MSG msg, wRemoveMsg: PM.PM_REMOVE))
{
if (msg.message == (uint)WindowMessage.WM_QUIT)
{
keepRunning = false;
break;
}
if (!handler.PreprocessMessage(msg))
{
TranslateMessage(msg);
DispatchMessage(msg);
}
}
}
}
}
}
catch (Exception e)
{
eventWindowReady.Set();
if (e is not (ThreadInterruptedException or ThreadAbortException))
System.Diagnostics.Debug.Fail("Unexpected thread exception in SystemEventHandler thread.", e.ToString());
}
handler.ThreadRunning.Set();
}
private void Init()
{
msgWindow = new BasicMessageWindow(MessageFilter);
OnMessageWindowHandleCreated();
}
}
internal static class UserSession
{
private static bool isUserInteractive;
private static HWINSTA processWinStation;
public static bool IsInteractive
{
get
{
if (Environment.OSVersion.Platform == System.PlatformID.Win32NT)
{
HWINSTA hwinsta = GetProcessWindowStation();
if (!hwinsta.IsNull && processWinStation != hwinsta)
{
using var flags = new SafeCoTaskMemStruct<USEROBJECTFLAGS>();
isUserInteractive = !GetUserObjectInformation((IntPtr)hwinsta, UserObjectInformationType.UOI_FLAGS, flags, flags.Size, out _) || (flags.Value.dwFlags & 0x0001 /*WSF_VISIBLE*/) != 0;
processWinStation = hwinsta;
}
}
else
{
isUserInteractive = true;
}
return isUserInteractive;
}
}
}
}