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
{
///
/// 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.
///
/// To use, derive a class and override the method. When handling the message, use the method to call the event.
///
///
/// Delegates can be registered and unregistered using unique GUID values via the and methods.
///
///
///
public abstract class SystemEventHandler : IDisposable
{
private static ManualResetEvent eventWindowReady;
private static Thread windowThread;
private readonly Dictionary eventHandles = new Dictionary();
private readonly object lockObj = new object();
private bool disposedValue;
private BasicMessageWindow msgWindow;
/// Initializes a new instance of the class.
///
/// if set to a new thread is created for the message pump regardless of the current apartment state.
///
/// System events not supported.
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();
}
}
/// Finalizes an instance of the class.
~SystemEventHandler()
{
Dispose(false);
}
/// Occurs when the message window handle has been created.
public event EventHandler MessageWindowHandleCreated;
/// Gets a value indicating whether this instance is running in a thread.
/// if this instance is running in a thread; otherwise, .
public bool IsRunningInThread => ThreadRunning is not null;
/// Gets the message window handle which can be used to register for messaged events.
/// The message window handle.
public HWND MessageWindowHandle => msgWindow?.Handle ?? HWND.NULL;
private ManualResetEvent ThreadRunning { get; set; }
/// Adds a delegate and its associated key to the handler list.
/// The key.
/// The delegate value.
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);
}
}
}
/// Removes a delegate and its associated key to the handler list.
/// The key.
/// The delegate value.
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);
}
}
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
void IDisposable.Dispose()
{
Dispose(true);
System.GC.SuppressFinalize(this);
}
/// Releases unmanaged and - optionally - managed resources.
///
/// to release both managed and unmanaged resources; to release only unmanaged resources.
///
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;
}
}
/// Determines whether the specified key has a delegate handler in its list.
/// The key.
/// if the specified key exists; otherwise, .
protected bool HasKey(Guid key)
{
lock (lockObj)
return eventHandles.ContainsKey(key);
}
/// Provides access to the WndProc listening for messages.
/// A handle to the window procedure that received the message.
/// The message.
/// Additional message information. The content of this parameter depends on the value of the Msg parameter.
/// Additional message information. The content of this parameter depends on the value of the Msg parameter.
/// The return value is the result of the message processing and depends on the message.
///
/// if this message should be considered handled; or to pass the message along to
/// .
///
protected abstract bool MessageFilter(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam, out IntPtr lReturn);
///
/// Allows derived classes to pre-process a message in a threaded message pump so that it is not passed along. Useful for things
/// like .
///
/// The MSG.
///
/// if the message was processed and should not be translated and dispatched;
/// otherwise. is the default.
///
protected virtual bool PreprocessMessage(in MSG msg) => false;
/// Called when an event has been added.
/// The event key.
protected virtual void OnEventAdd(Guid key)
{
}
/// Called when an event has been removed.
/// The event key.
protected virtual void OnEventRemove(Guid key)
{
}
/// Called when the message window handle is created.
protected virtual void OnMessageWindowHandleCreated() => MessageWindowHandleCreated?.Invoke(this, EventArgs.Empty);
/// Calls the delegate list associated with the key, passing the supplied parameters.
/// The key.
/// The arguments.
/// The value returned by the call to the delegate list.
/// Event for {key} is not registered.
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();
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;
}
}
}
}