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; } } } }