База знаний KB0201

Использование подклассов Windows при программировании надстройки .NET

Проблема

Я разрабатываю надстройку .NET для Microsoft Office, в которой используются подклассы Windows. У моих клиентов аварийно завершается работа Office, когда запущена моя и другая надстройка (например, think-cell).

Решение

Эта проблема часто возникает из-за использования подклассов с помощью NativeWindow.AssignHandle / NativeWindow.ReleaseHandle.

Вместо этого следуйте рекомендациям Microsoft и вызовите неуправляемый код (P/Invoke) в Comctl32.dll's SetWindowSubclass и RemoveWindowSubclass, как описано в этой статье блога Microsoft. Чтобы внести это изменение в вашем проекте, замените NativeWindow на thinkcell.SubclassedWindow.cs там, где NativeWindow используется для подклассов:

using System;
using System.Diagnostics;

namespace thinkcell
{

    internal enum BOOL : int
    {
        FALSE = 0,
        TRUE = 1,
    }

    internal static partial class ComCtl32
    {
        public delegate IntPtr SUBCLASSPROC(
            IntPtr hWnd,
            int msg,
            IntPtr wParam,
            IntPtr lParam,
            UIntPtr uIdSubclass,
            UIntPtr dwRefData
        );

        [System.Runtime.InteropServices.DllImport("comctl32.dll", ExactSpelling = true)]
        public static extern BOOL SetWindowSubclass(
            IntPtr hWnd,
            IntPtr pfnSubclass,
            UIntPtr uIdSubclass,
            UIntPtr dwRefData
        );

        [System.Runtime.InteropServices.DllImport("comctl32.dll", ExactSpelling = true)]
        public static extern BOOL RemoveWindowSubclass(
            IntPtr hWnd,
            IntPtr pfnSubclass,
            UIntPtr uIdSubclass
        );

        [System.Runtime.InteropServices.DllImport("comctl32.dll", ExactSpelling = true)]
        public static extern IntPtr DefSubclassProc(
            IntPtr hWnd,
            int msg,
            IntPtr wParam,
            IntPtr lParam
        );
    }

    class SubclassedWindow : MarshalByRefObject, System.Windows.Forms.IWin32Window
    {
        // prevents collection of SubclassedWindow that is still in use
        static private System.Collections.Generic.HashSet _instancesInUse = new System.Collections.Generic.HashSet();

        // The number of uses we still have for this instances:
        // - some window attached, or
        // - inside a window procedure
        private int _uses = 0;

        // Our window procedure delegate
        private ComCtl32.SUBCLASSPROC _windowProc;

        // The native handle for our delegate
        private IntPtr _windowProcHandle;

        static SubclassedWindow()
        {
            AppDomain.CurrentDomain.ProcessExit += OnShutdown;
        }

        public SubclassedWindow()
        {
            _windowProc = new ComCtl32.SUBCLASSPROC(Callback);
            _windowProcHandle = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(_windowProc);
        }

        /// 
        ///  Gets the handle for this window.
        /// 
        public IntPtr Handle { get; private set; }

        /// 
        ///  Assigns a handle to this  instance.
        /// 
        public void AssignHandle(IntPtr handle)
        {
            CheckReleased();
            Debug.Assert(handle != IntPtr.Zero, "handle is 0");

            if (0 == _uses)
            {
                lock(_instancesInUse)
                {
                    _instancesInUse.Add(this);
                }
            } // else may happen if handle gets reassigned inside WndProc.
            // This is legal after any call to DefWndProc.

            ++_uses;
            Handle = handle;

            ComCtl32.SetWindowSubclass(handle, _windowProcHandle, UIntPtr.Zero, UIntPtr.Zero);
            OnHandleChange();
        }

        /// 
        ///  Window message callback method. Control arrives here when a window
        ///  message is sent to this Window. This method packages the window message
        ///  in a Message object and invokes the WndProc() method. A WM_NCDESTROY
        ///  message automatically causes the ReleaseHandle() method to be called.
        /// 
        private IntPtr Callback(
            IntPtr hWnd,
            int msg,
            IntPtr wParam,
            IntPtr lParam,
            UIntPtr uIdSubclass,
            UIntPtr dwRefData
        )
        {
            Debug.Assert(0 < _uses);
            ++_uses;

            try
            {
                var m = System.Windows.Forms.Message.Create(hWnd, msg, wParam, lParam);
                WndProc(ref m);
                return m.Result;
            }
            catch (Exception e)
            {
                OnThreadException(e);
                return IntPtr.Zero;
            }
            finally
            {
                if (msg == 0x82/*WM_NCDESTROY*/ && Handle != IntPtr.Zero) {
                    InternalReleaseHandle();
                }
                if (0 == --_uses)
                {
                    lock (_instancesInUse)
                    {
                        _instancesInUse.Remove(this);
                    }
                }
            }
        }

        /// 
        ///  Raises an exception if the window handle is not zero.
        /// 
        private void CheckReleased()
        {
            if (Handle != IntPtr.Zero)
            {
                throw new InvalidOperationException("Window handle already exists.");
            }
        }

        /// 
        ///  Invokes the default window procedure associated with this Window. It is
        ///  an error to call this method when the Handle property is zero.
        /// 
        public void DefWndProc(ref System.Windows.Forms.Message m)
        {
            Debug.Assert(m.HWnd==Handle, "SubclassedWindow is not attached to the window m is addressed to.");
            m.Result = ComCtl32.DefSubclassProc(m.HWnd, m.Msg, m.WParam, m.LParam);
        }

        /// 
        ///  Specifies a notification method that is called when the handle for a
        ///  window is changed.
        /// 
        protected virtual void OnHandleChange()
        {
        }

        /// 
        ///  On class load, we connect an event to Application to let us know when
        ///  the process or domain terminates.  When this happens, we attempt to
        ///  clear our window class cache.  We cannot destroy windows (because we don't
        ///  have access to their thread), and we cannot unregister window classes
        ///  (because the classes are in use by the windows we can't destroy).  Instead,
        ///  we move the class and window procs to DefWndProc
        /// 
        [System.Runtime.ConstrainedExecution.PrePrepareMethod]
        private static void OnShutdown(object sender, EventArgs e)
        {
            // No lock because access here should be race-free, no concurrent SubclassedWindow.AttachHandle/ReleaseHandle
            // should happen while shutting down.
            Debug.Assert(0 == _instancesInUse.Count);
        }

        /// 
        ///  When overridden in a derived class, manages an unhandled thread exception.
        /// 
        protected virtual void OnThreadException(Exception e)
        {
        }

        private void InternalReleaseHandle()
        {
            Debug.Assert(Handle != IntPtr.Zero);
            ComCtl32.RemoveWindowSubclass(Handle, _windowProcHandle, UIntPtr.Zero);
            Handle = IntPtr.Zero;
            OnHandleChange();
            --_uses;
        }

        /// 
        ///  Releases the handle associated with this window.
        /// 
        public void ReleaseHandle()
        {
            if (Handle != IntPtr.Zero) {
                InternalReleaseHandle();
                if (0 == _uses)
                {
                    lock (_instancesInUse)
                    {
                        _instancesInUse.Remove(this);
                    }
                }
            }
        }

        /// 
        ///  Invokes the default window procedure associated with this window.
        /// 
        protected virtual void WndProc(ref System.Windows.Forms.Message m)
        {
            DefWndProc(ref m);
        }
    }
}

Скачать SubclassedWindow.cs

Ограничения.

  • Исходный NativeWindow также может создавать окна, но это редко используется вместе с подклассами в том же экземпляре NativeWindow.
  • thinkcell.SubclassedWindow не является потокобезопасным, но создание подклассов и обработка сообщений обычно происходит в том же потоке.

Если у вас возникли проблемы с использованием thinkcell.SubclassWindow, сообщите нам.

think-cell использует файлы cookie для улучшения функциональности, эффективности и безопасности этого веб-сайта. Если вы хотите пользоваться всеми функциями этого сайта, требуется ваше согласие. С более подробной информацией об использовании файлов cookie компанией think-cell, вашем согласии и правах на конфиденциальность данных можно ознакомиться в нашей Политике конфиденциальности.