|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
namespace System.Windows.Forms;
/// <summary>
/// Allows to subclass individual windows.
/// </summary>
/// <remarks>
/// <para>
/// To actually subclass the window, call <see cref="Open"/>. To ensure the
/// subclassing is correctly undone, you must call <see cref="Dispose()"/> before the
/// window is destroyed.
/// </para>
/// <para>
/// See https://docs.microsoft.com/en-us/windows/desktop/winmsg/about-window-procedures#instance-subclassing
/// for more information about subclassing.
/// </para>
/// </remarks>
internal class WindowSubclassHandler : IDisposable
{
private readonly HWND _handle;
private bool _opened;
private bool _disposed;
private unsafe void* _originalWindowProc;
/// <summary>
/// The delegate for <see cref="WndProc(ref Message)"/>
/// which is marshaled as native callback.
/// </summary>
/// <remarks>
/// <para>
/// We must store this delegate (and prevent it from being garbage-collected)
/// to ensure the function pointer doesn't become invalid.
/// </para>
/// <para>
/// Note: We create a new delegate (and native function pointer) for each
/// instance because even though creation will be slower (and requires a
/// bit of memory to store the native code) it will be faster when the window
/// procedure is invoked, because otherwise we would need to use a dictionary
/// to map the hWnd to the instance, as the window procedure doesn't allow
/// to store reference data. However, creating a new delegate for each instance
/// is also the way that the <see cref="NativeWindow"/> class does it.
/// </para>
/// </remarks>
private readonly WNDPROC _windowProcDelegate;
/// <summary>
/// The function pointer created from <see cref="_windowProcDelegate"/>.
/// </summary>
private readonly unsafe void* _windowProcDelegatePtr;
/// <summary>
/// Initializes a new instance of the <see cref="WindowSubclassHandler"/> class.
/// </summary>
/// <param name="hwnd">The window handle of the window to subclass.</param>
public unsafe WindowSubclassHandler(HWND hwnd)
{
if (hwnd.IsNull)
{
throw new ArgumentNullException(nameof(hwnd));
}
_handle = hwnd;
// Create a delegate for our window procedure and get a function pointer for it.
_windowProcDelegate = NativeWndProc;
_windowProcDelegatePtr = (void*)Marshal.GetFunctionPointerForDelegate(_windowProcDelegate);
}
/// <summary>
/// Subclasses the window.
/// </summary>
/// <remarks>
/// <para>
/// You must call <see cref="Dispose()"/> to undo the subclassing before the window is destroyed.
/// </para>
/// </remarks>
/// <exception cref="Win32Exception">The window could not be subclassed.</exception>
/// <exception cref="InvalidOperationException"><see cref="Open"/> was already called.</exception>
public unsafe void Open()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_opened)
{
throw new InvalidOperationException();
}
// Replace the existing window procedure with our one ("instance subclassing").
// Note: It shouldn't be possible to set a null pointer as window procedure, so we
// can use the return value to determine if the call succeeded.
_originalWindowProc = (void*)PInvokeCore.SetWindowLong(
_handle,
WINDOW_LONG_PTR_INDEX.GWL_WNDPROC,
(nint)_windowProcDelegatePtr);
if (_originalWindowProc is null)
{
throw new Win32Exception();
}
Debug.Assert(_originalWindowProc != _windowProcDelegatePtr);
_opened = true;
}
/// <summary>
/// Releases all resources used by the <see cref="WindowSubclassHandler"/>.
/// </summary>
/// <remarks>
/// <para>
/// This method undoes the subclassing that was initiated by calling <see cref="Open"/>.
/// You must call this method before the window that was subclassed is destroyed.
/// </para>
/// <para>
/// If undoing the subclassing fails, this method will throw an exception. In that case,
/// you should call <see cref="KeepCallbackDelegateAlive"/> after the window is destroyed
/// to ensure the managed callback delegate is kept alive until the window procedure will
/// no longer be called.
/// </para>
/// </remarks>
/// <exception cref="Win32Exception">The subclassing could not be undone.</exception>
/// <exception cref="InvalidOperationException">
/// The current window procedure is not the expected one.
/// </exception>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Keeps the managed callback delegate alive from which the native function pointer
/// for the window procedure is created.
/// </summary>
/// <remarks>
/// <para>
/// When subclassing a window, a native function pointer is created from a managed
/// callback delegate which is then set as the window procedure. The callback is
/// automatically kept alive until <see cref="Dispose()"/> is called to undo the
/// subclassing.
/// </para>
/// <para>
/// However, if <see cref="Dispose()"/> fails (indicated by throwing an exception),
/// e.g. because the current window procedure pointer is not the expected one,
/// you should call this method after the window is actually destroyed, to ensure
/// the callback delegate is kept alive up to that time. Failing to do this might
/// result in undefined behavior.
/// </para>
/// </remarks>
public void KeepCallbackDelegateAlive()
{
GC.KeepAlive(_windowProcDelegate);
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="WindowSubclassHandler"/> and
/// optionally releases the 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>
/// <exception cref="Win32Exception">The subclassing could not be undone.</exception>
/// <exception cref="InvalidOperationException">
/// The current window procedure is not the expected one.
/// </exception>
protected virtual unsafe void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
// We cannot do anything from the finalizer thread since we have
// resources that must only be accessed from the GUI thread.
if (disposing && _opened)
{
// Check if the current window procedure is the correct one.
void* currentWindowProcedure = (void*)PInvokeCore.GetWindowLong(
_handle,
WINDOW_LONG_PTR_INDEX.GWL_WNDPROC);
if (currentWindowProcedure == null)
{
throw new Win32Exception();
}
if (currentWindowProcedure != _windowProcDelegatePtr)
{
// This can mean other code has also subclassed the window but failed
// to undo it.
// Note: Instead of failing, we could simply cut off the subclass chain and
// always restore the original window procedure here.
throw new InvalidOperationException(SR.WindowSubclassHandlerWndProcIsNotExceptedOne);
}
// Undo the subclassing by restoring the original window
// procedure.
if (PInvokeCore.SetWindowLong(
_handle,
WINDOW_LONG_PTR_INDEX.GWL_WNDPROC,
(nint)_originalWindowProc) == 0)
{
throw new Win32Exception();
}
// Ensure to keep the delegate alive up to the point after we
// have undone the subclassing.
KeepCallbackDelegateAlive();
}
_disposed = true;
}
/// <summary>
/// Processes Windows messages for the subclassed window.
/// </summary>
/// <param name="m">The message to process.</param>
protected virtual unsafe void WndProc(ref Message m)
{
// Call the original window procedure to process the message.
if (_originalWindowProc is not null)
{
m.ResultInternal = PInvokeCore.CallWindowProc(
_originalWindowProc,
m.HWND,
(uint)m.Msg,
m.WParamInternal,
m.LParamInternal);
}
}
/// <summary>
/// Determines if the specified <paramref name="exception"/> that was thrown
/// by <see cref="WndProc(ref Message)"/> shall be caught and passed to
/// <see cref="HandleWndProcException(Exception)"/>.
/// </summary>
/// <param name="exception"></param>
/// <returns>
/// <see langword="true"/> to catch the exception, or <see langword="false"/> to let it bubble up to the caller.
/// </returns>
protected virtual bool CanCatchWndProcException(Exception exception)
{
// By default, don't catch exceptions.
return false;
}
/// <summary>
/// Called when an exception thrown by <see cref="WndProc(ref Message)"/> was caught.
/// </summary>
/// <param name="exception">The <see cref="Exception"/> that was caught.</param>
protected virtual void HandleWndProcException(Exception exception)
{
// Simply rethrow the exception here.
ExceptionDispatchInfo.Throw(exception);
}
private LRESULT NativeWndProc(
HWND hWnd,
uint msg,
WPARAM wParam,
LPARAM lParam)
{
Debug.Assert(hWnd == _handle);
Message m = Message.Create(hWnd, msg, wParam, lParam);
try
{
WndProc(ref m);
}
catch (Exception ex) when (CanCatchWndProcException(ex))
{
HandleWndProcException(ex);
}
return m.ResultInternal;
}
}
|