Autodesk Inventor API Hacking (Dockable Windowでのキー入力)

# 0. はじめに
近年のInventorでは簡単にDockable Windowを作成できるのですが、`Enter`キーと`ESC`キーが入力できずにハマるというパターンがあると思います。
これについて、回避方法を説明します。

# 1. そもそも、この問題は何故発生する?
WinFormsを使って開発すると、それぞれのControlが特殊キー(`Enter`や`ESC`)を受け取った時に、親Formにお伺いを立てるわけですが、その近辺の処理がうまく出来ないと、この問題が発生するようです。

# 2. 回避策 ～ Formに配置しよう
この問題が発生するのは、DocableWindowにControlを直接登録した場合です。
逆を言えば、ControlをいったんFormに配置して、そのFormを登録すれば、問題をひとまずは回避できます。

```C#
public void Activate(Inventor.ApplicationAddInSite addInSiteObject, bool firstTime)
{
    DockableWindow dockWindow;

    // (中略)

    Form1 frm = new Form1();

    dockWindow.AddChild(frm.Handle);
    dockWindow.Visible = true;
    dockWindow.SetMinimumSize(200, 300);

    frm.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
    frm.Show();
}
```

しかし、この方法では、DocableWindowがInventorとは別のWindowとして振舞おうとするので、微妙にフォーカスがらみで挙動がおかしくなります。
具体的には、InventorのWindowが例えばメモ帳の後ろにある場合、このDocableWindowをクリックしてもInventorが前面に来ません。

# 3. 回避策 ～ WM_GETDLGCODEに対し、DLGC_WANTALLKEYSを返そう
RichTextBoxでしか試していませんが、殆どの場合はこれで回避できると思います。

```C#:MyRichTextBox.cs
using System;

namespace InvAddIn
{
    class MyRichTextBox : System.Windows.Forms.RichTextBox
    {
        private const int WM_GETDLGCODE = 0x0087;
        private const int DLGC_WANTALLKEYS = 0x0004;

        protected override void WndProc(ref System.Windows.Forms.Message m)
        {
            switch (m.Msg)
            {
                case WM_GETDLGCODE:
                    if (m.LParam != null)
                        m.Result = (IntPtr)DLGC_WANTALLKEYS;
                    break;
                default:
                    base.WndProc(ref m);
                    break;
            }
        }
    }
}
```

技術的な詳細は割愛しますが、RichTextBoxの代わりに上記のMyRichTextBoxを使うと、`Enter`キーをキャプチャーできるようになります。
実際の場面では、UserControlに配置して使うのでしょうから、そのUserControl自体にこの方法を適用すれば良いのでは? と思い試してみましたが、駄目でした。
ですので、UserControl内の全てのControlは、それぞれに対策が必要です。(もちろん、必要がある場合は、ですけど)

# 4. 回避策 ～ WindowsのEventをHookしよう
WM_GETDLGCODEに対応することで解決したかに思えたのですが、DataGridViewではうまく行きませんでした。
追いかけてみると、DataGridViewは、編集時にそのセルに重ねるように別のTextBoxを配置していたのです。
このTextBoxのWndProcに介入できないので、DataGridViewについては、大掛かりになりますがWindows自体のKey EventをHookします。
少しcodeが長いですが、お付き合いください。

```C#:MyDataGridView.cs
class MyDataGridView : System.Windows.Forms.DataGridView
{
#region Win32 API declare
    private static class NativeMethods
    {
        [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
        internal static extern System.IntPtr SetWindowsHookEx(int hookType, HookObject.HookHandler hookDelegate, System.IntPtr module, uint threadId);
        [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
        internal static extern bool UnhookWindowsHookEx(System.IntPtr hook);

        [System.Runtime.InteropServices.DllImport("user32.dll")]
        internal static extern int CallNextHookEx(System.IntPtr hook, int code, System.IntPtr wParam, System.IntPtr lParam);
        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        internal static extern uint GetCurrentThreadId();
        [System.Runtime.InteropServices.DllImport("user32.dll")]
        internal static extern System.IntPtr GetFocus();
        [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
        internal static extern System.IntPtr SendMessage(System.IntPtr hWnd, int Msg, System.IntPtr wParam, System.IntPtr lParam);
    }
    private const int WH_KEYBOARD = 2;
    private const int VK_CANCEL = 0x03;
    private const int VK_RETURN = 0x0d;
    private const int VK_ESCAPE = 0x1b;
    private const int WM_KEYDOWN = 0x0100;
#endregion
#region Key Hook
    private class HookObject
    {
        public delegate int HookHandler(int code, System.IntPtr wParam, System.IntPtr lParam);
        public HookHandler hookDelegate;
        public System.IntPtr hook;

        public void SetHook(HookHandler onHook)
        {
            // Check this control is in Visual Studio.
            bool inDesignMode;
            if (System.ComponentModel.LicenseManager.UsageMode == System.ComponentModel.LicenseUsageMode.Designtime)
            {
                inDesignMode = true;
            }
            else
            {
                using (var p = System.Diagnostics.Process.GetCurrentProcess())
                {
                    inDesignMode = (
                        p.ProcessName.Equals("DEVENV", System.StringComparison.OrdinalIgnoreCase) ||
                        p.ProcessName.Equals("XDesProc", System.StringComparison.OrdinalIgnoreCase)
                    );
                }
            }
            // Execute only outside of Visual Studio.
            if (!inDesignMode)
            {
                if (hook == System.IntPtr.Zero)
                {
                    hookDelegate = new HookHandler(onHook);
                    hook = NativeMethods.SetWindowsHookEx(WH_KEYBOARD, hookDelegate, System.IntPtr.Zero, NativeMethods.GetCurrentThreadId());
                    if (hook == System.IntPtr.Zero)
                    {
                        int errorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
                        throw new System.ComponentModel.Win32Exception(errorCode);
                    }
                }
            }
        }
        public void Unhook()
        {
            if (hook != System.IntPtr.Zero)
            {
                NativeMethods.UnhookWindowsHookEx(hook);
            }
        }
        public HookObject(HookHandler onHook)
        {
            SetHook(onHook);
        }

        ~HookObject()
        {
            Unhook();
        }

    }
    private static readonly HookObject hookObject = new HookObject(OnHookKey);

    private static System.IntPtr hwndKeyHook = System.IntPtr.Zero;

    private static int OnHookKey(int nCode, System.IntPtr wParam, System.IntPtr lParam)
    {
        if (-1 < nCode)
        {
            int vKey = (int)wParam;
            ulong keyFlag = (ulong)lParam;
            switch (vKey)
            {
                case VK_RETURN:
                case VK_CANCEL:
                case VK_ESCAPE:
                    if ((keyFlag & 0xc0000000) == 0 && hwndKeyHook == NativeMethods.GetFocus())
                    {
                        NativeMethods.SendMessage(hwndKeyHook, WM_KEYDOWN, wParam, lParam);
                        return 1;
                    }
                    break;
            }
        }
        return NativeMethods.CallNextHookEx(hookObject.hook, nCode, wParam, lParam);
    }
#endregion
#region Event
    private static void Controls_GotFocus(object sender, System.EventArgs e)
    {
        if (sender is System.Windows.Forms.Control control)
        {
            hwndKeyHook = control.Handle;
        }
    }

    private static void Controls_LostFocus(object sender, System.EventArgs e)
    {
        hwndKeyHook = System.IntPtr.Zero;
    }

    protected override void OnEditingControlShowing(System.Windows.Forms.DataGridViewEditingControlShowingEventArgs e)
    {
        if (e.Control is System.Windows.Forms.DataGridViewTextBoxEditingControl textBox)
        {
            textBox.GotFocus -= Controls_GotFocus;
            textBox.GotFocus += Controls_GotFocus;
            textBox.LostFocus -= Controls_LostFocus;
            textBox.LostFocus += Controls_LostFocus;
        }
        base.OnEditingControlShowing(e);
    }
#endregion
}
```

この対策をUserControlに適用すれば、全て解決するのでは? と思ったのですが、DataGridViewに対しては良かったものの、今度はRichTextBoxに対してうまく動作しませんでした。
もう少し突っ込んでいけば、UserControlだけの対応ですむ方法が見つかったのかもしれません。しかし、上述の通りRichTextBoxには既に回避策が見つかっていたので、個別対応することにしました。(この時点で、既に結構な日数を消費していたので、力尽きたのです)

上記code内で、AddInがunloadされるときにHookを解放しようとしています。(HookObjectのデストラクター)
しかし、実際にはこの解放するcodeは呼ばれていないようです。Inventorが正しい手順を踏んでAddInのtheradを終了させていないのかもしれません。
現時点では何の問題も発生していないので、WindowsがAddInのtheradが終了するときにunhookしてくれているようです。

# 5. 自作Controlを使う場合の注意点 (x32/x64問題)
通常は気にすることが無いのですが、今回の解決策のように標準のWinFormsではない自作Controlを使って開発しようとする場合は、`対象プラットフォーム`に`Any CPU`ではなく`x64`を指定していると、ハマります。
具体的には、Visual Studioの`デザイナー`でControlを配置できなくなります。
これは、`デザイナー`が実際にControlをロード(実行)して画面上に配置するからで、`x32`アプリケーションであるVisual Studioは`x64`のカスタムControlを読み込めないからです。
解決策としては、`デザイナー`で作業する前に`Any CPU`にしてリビルドすると良いです。`デザイナー`で作業しない限りは、`x64`で作業して問題ありません。

# 99. 親の記事に戻る
[Autodesk Inventor API Hacking (概略)](https://qiita.com/yamarah/items/cee08eb8f2b7aecbda83)