#0. はじめに
近年のInventorでは簡単にDockable Windowを作成できるのですが、Enter
キーとESC
キーが入力できずにハマるというパターンがあると思います。
これについて、回避方法を説明します。
#1. そもそも、この問題は何故発生する?
WinFormsを使って開発すると、それぞれのControlが特殊キー(Enter
やESC
)を受け取った時に、親Formにお伺いを立てるわけですが、その近辺の処理がうまく出来ないと、この問題が発生するようです。
#2. 回避策 ~ Formに配置しよう
この問題が発生するのは、DocableWindowにControlを直接登録した場合です。
逆を言えば、ControlをいったんFormに配置して、そのFormを登録すれば、問題をひとまずは回避できます。
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でしか試していませんが、殆どの場合はこれで回避できると思います。
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が長いですが、お付き合いください。
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 (概略)