2012年8月3日金曜日

Windows7で VB6 / VBA の SendKeys の問題について ( エラー 70 書き込みできません。 が発生)


Windows7 VB6 / VBA の SendKeys の問題について ( エラー 70 書き込みできません。 が発生)


Windows7で仮想化したVB6製のプログラムや、Excel2000 、Access2000 等のOffice2000を実行しVBAのマクロを動かした場合に、エラーが発生することがあります。

具体的には、SendKeys というファンクションをプログラムの中で利用している場合です。
SendKeys は、キーボードのキーを押した事にするためのファンクションです。

キーボードのaを押した事にする場合はこんな感じでコードを書きます。
Call SendKeys("a")


------宣伝-----
※VMware ThinApp (アプリケーション仮想化)に興味のある方は、こちらを
御覧ください。

[VMware ThinApp 製品概要と活用 解説書 (日本語 SoftBankBB製) の紹介]

http://tunemicky.blogspot.jp/2013/08/vmware-thinapp-softbankbb.html
------宣伝-----

始めに、SendKeys の問題は、仮想アプリケーションの問題ではありません。
ですので、ThinAppの問題ではありません。

Windows7の仕様によるものです。

正確には、Windows Vistaから、UACが搭載されUIPIという物が導入されました。
セキュリティの強化に伴う制限事項であり、仕様変更です。
詳しい説明は、以下のリンクから

対処方法は、UACを無効にすることになりますが、せっかくのセキュリティの強化機能を
無効化するのは、おすすめしません。

対処方法としては、以下になります。
・UACを無効にする (お勧めしません。)
・コードを書き換える (お勧めですが。。。)
SendInput へとコードを修正します。

・Windows7に対応したバージョンを利用する。
VB6の場合
MSVBVM60.DLL
項目
バージョン
リリース
SendKeys
Windows7 SP1標準
6.00.9815
March 5, 2009
問題なし
WindowsXP SP3標準
6.00.9802
August 1, 2007
問題なし
Windows2000 SP4標準
6.00.9690
September 3, 2002
エラー
VisualBasic6 SP6 標準
6.00.9782
February 23, 2004
エラー
※Windows7標準のMSVBVM60.dllは対応されているので、問題ありません。
 仮想アプリケーションを作成した環境により、非対応のモジュールがキャプチャされ
 動作に影響をおよぼすことが考えられます。
例)Windows 2000 環境でキャプチャを行い、VB6 SP6のランタイムをインストール
した場合に、%SystemSystem%フォルダに、対応していないバージョンの
MSVBVM60.dllが含まれてしまう可能性があります。
この場合は、対応するバージョンに変更するか、
MSVBVM60.dll自体を削除することで問題が無くなります。

VBAの場合
VBE6.DLL
項目
バージョン
リリース
SendKeys
Office2000 SP4
6.00.8967
August 24, 2000
エラー
OfficeXP SP3
6.04.9969
June 30, 2003
エラー
OfficeXP SP3
(+最新パッチ)
6.05.1053
August 25, 2009
問題なし
Office2003 SP3
6.05.1024
May 8, 2007
問題なし
Office2007 SP3
6.05.1053
August 25, 2009
問題なし

※バージョン6.05.xxxx系は、問題ないように対策されています。
Office2007 以降は、問題ありません。

2012/08/14追加:
OfficeXP SP3の場合は、対策されていません。

そのため、OfficeXP SP3を更新して最新の状態にすると、対策されたバージョンがインストールされます。

 無保証ですが、Office2000の場合は、VBE6.DLLを対策された物に更新したところ
 問題が無くなりました。



せっかくなので、もう少し詳細に調査したことを書きたいと思います。

始めに以下のリンクを参照ください。
「Windows Vista 上の Office アプリケーションで SendKeys ステートメントが失敗する」

「この問題は、Office 2003 で使用される VBA 6.0 の SendKeys ステートメントにおいて、
ジャーナル フックが使用されているために発生します。
Windows Vista の新しいセキュリティ機能では、ジャーナル フックが利用できません。」


「ジャーナル フック」がポイントです。
キーボードの情報を送信するときの手法は、いくつかあります。

問題になっているのは、VB6 / VBAの中で SendKeys を呼び出した場合ですが
実際に、どのような呼び出しになっているかを確認すると
以下の用になっています。


1.送信するキーをキーボードに対応した仮想キーコードへと変換
VkKeyScanW ->short=41h (WCHAR ch=L'a')

2.仮想キーの状態を取得
GetKeyboardState ->int=1h (PBYTE lpKeyState=*18DA8Ch->'\00h')

3.フックプロシージャを使って、キーボードをフックします。
SetWindowsHookExW (int idHook=1h, HOOKPROC lpfn=*69208967h, HINSTANCE hmod=400000h, struct {DWORD dwThreadId=0h)

※このSetWindowsHookExW で、エラーが発生します。
第1引数を確認すると、int idHook=1h
になっています。

WinUser.hで確認すると、
#define WH_JOURNALPLAYBACK  1 
となっています。

MSDNライブラリには、
WH_JOURNALPLAYBACKは、「グローバルのみ」と書かれているため
権限がそれなりに必要です。
なので、「エラー70番 書き込みできません。」が発生することになります。

http://msdn.microsoft.com/ja-jp/library/aa480152.aspx#EKFAG
を確認すると、まさに制限事項として書かれています。
抜粋すると

ジャーナル フック
WH_JOURNALPLAYBACK と WH_JOURNALRECORD は本質的にプロセス間で実行されるため、
最高の権限レベルが必要です。
多くの場合にすべての UI 権限が必要なわけではない SendInput() API をジャーナル フックの代わりに使用することができます。
WH_JOURNALPLAYBACK および WH_JOURNALRECORD ジャーナル フックは、
本質的にはプロセス間のメッセージングであるため、
より高い権限が必要です。ジャーナル フックの別の方法として、
SendInput() 関数がありますが、これは多くの場合フル UI 権限を必要としません。


次に、対策されたバージョンの呼び出しがどのようになっているか確認すると
以下の用になっています。

1.送信するキーをキーボードに対応した仮想キーコードへと変換
2.仮想キーの状態を取得
3.フックプロシージャを使って、キーボードをフックします。
までは、同じです。

3.でエラーが発生した場合は、更に以下の用に対策されています。

4.キーボードの入力をブロックします。
BlockInput ->int=0h (BOOL fBlockIt=1h)

5.仮想キーの状態を取得
GetKeyboardState ->int=1h (PBYTE lpKeyState=*18D904h->'\00h')

6.SendInputを利用しキーボード入力を行います。
SendInput (UINT cInputs=1h, *pInputs=*18DA04h->struct {DWORD type=1h}, int cbSize=1Ch)

というわけで、対策されたものは、SetWindowsHookExでエラーが発生した後に
SendInputを利用して、処理を続行してくれていることがわかりました。

調査した際に利用した、ログは以下になります。
VB6で作成したプログラムを実行した結果、問題なく動作しているログとなります。

MSVBVM60.DLL:->USER32.dll      VkKeyScanW (WCHAR ch=L'a')
MSVBVM60.DLL:<-USER32.dll      VkKeyScanW ->short=41h (WCHAR ch=L'a')
MSVBVM60.DLL:->kernel32.dll    GlobalAlloc (UINT uFlags=0h, SIZE_T dwBytes=20h)
MSVBVM60.DLL:<-kernel32.dll    GlobalAlloc ->*=*885A10h (UINT uFlags=0h, SIZE_T dwBytes=20h)
MSVBVM60.DLL:->kernel32.dll    GlobalLock (HGLOBAL hMem=885a10h)
MSVBVM60.DLL:<-kernel32.dll    GlobalLock ->*=*885A10h (HGLOBAL hMem=885a10h)
MSVBVM60.DLL:->USER32.dll      GetKeyboardState (PBYTE lpKeyState=*18DA8Ch->'@')
MSVBVM60.DLL:<-USER32.dll      GetKeyboardState ->int=1h (PBYTE lpKeyState=*18DA8Ch->'\00h')
kernel32.dll:->ntdll.dll         RtlSetLastWin32Error (DWORD =0h)
kernel32.dll:<-ntdll.dll         RtlSetLastWin32Error ->void=void (DWORD =0h)
kernel32.dll:->ntdll.dll         RtlFreeHeap (HANDLE =850000h, ULONG =0h, PVOID =*885F28h)
kernel32.dll:<-ntdll.dll         RtlFreeHeap ->char='\01h' (HANDLE =850000h, ULONG =0h, PVOID =*885F28h)
kernel32.dll:->KERNELBASE.d      GetFullPathNameA (== no prototype available ==)
kernel32.dll:<-KERNELBASE.d      GetFullPathNameA (== no prototype available ==)
kernel32.dll:->ntdll.dll         RtlSetLastWin32Error (DWORD =2h)
kernel32.dll:<-ntdll.dll         RtlSetLastWin32Error ->void=void (DWORD =2h)
MSVBVM60.DLL:->kernel32.dll    GetModuleHandleA+ (LPCSTR lpModuleName=*0h )
MSVBVM60.DLL:<-kernel32.dll    GetModuleHandleA+ ->*=*400000h->struct {int unused=905A4Dh} (LPCSTR lpModuleName=*0h )
MSVBVM60.DLL:->USER32.dll      SetWindowsHookExW+ (int idHook=1h, HOOKPROC lpfn=*72A0DF35h, HINSTANCE hmod=400000h, struct {DWORD dwThreadId=0h)
MSVBVM60.DLL:<-USER32.dll      *** SetWindowsHookExW+ ->*=*0h  (int idHook=1h, HOOKPROC lpfn=*72A0DF35h, HINSTANCE hmod=400000h, struct {DWORD dwThreadId=0h) *** GetLastError() returns 5=0x5 [2]:
MSVBVM60.DLL:->kernel32.dll    LoadLibraryA+ (LPCSTR lpLibFileName=*7296EB24h->"user32.dll")
kernel32.dll:->ntdll.dll         _strcmpi (*=*2030Ch->'u', *=*767F49E0h->'t')
kernel32.dll:<-ntdll.dll         _strcmpi ->int=18D6ACh (*=*2030Ch->'u', *=*767F49E0h->'t')
MSVBVM60.DLL:<-kernel32.dll    LoadLibraryA+ ->*=*750E0000h->struct {int unused=905A4Dh} (LPCSTR lpLibFileName=*7296EB24h->"user32.dll")
MSVBVM60.DLL:->kernel32.dll    GetProcAddress+ (HMODULE hModule=750e0000h, struct {LPCSTR lpProcName=*7296EB18h->"SendInput")
kernel32.dll:->ntdll.dll         RtlFreeHeap (HANDLE =2790000h, ULONG =0h, PVOID =*27E0DC8h)
kernel32.dll:<-ntdll.dll         RtlFreeHeap ->char='\01h' (HANDLE =2790000h, ULONG =0h, PVOID =*27E0DC8h)
kernel32.dll:->ntdll.dll         RtlFreeHeap (HANDLE =2790000h, ULONG =0h, PVOID =*2801758h)
kernel32.dll:<-ntdll.dll         RtlFreeHeap ->char='\01h' (HANDLE =2790000h, ULONG =0h, PVOID =*2801758h)
MSVBVM60.DLL:<-kernel32.dll    GetProcAddress+ ->*=*7EEF1DF4h (HMODULE hModule=750e0000h, struct {LPCSTR lpProcName=*7296EB18h->"SendInput")
MSVBVM60.DLL:->kernel32.dll    GetProcAddress+ (HMODULE hModule=750e0000h, struct {LPCSTR lpProcName=*7296EB0Ch->"BlockInput")
kernel32.dll:->ntdll.dll         RtlFreeHeap (HANDLE =2790000h, ULONG =0h, PVOID =*27E0DC8h)
kernel32.dll:<-ntdll.dll         RtlFreeHeap ->char='\01h' (HANDLE =2790000h, ULONG =0h, PVOID =*27E0DC8h)
kernel32.dll:->ntdll.dll         RtlFreeHeap (HANDLE =2790000h, ULONG =0h, PVOID =*2801758h)
kernel32.dll:<-ntdll.dll         RtlFreeHeap ->char='\01h' (HANDLE =2790000h, ULONG =0h, PVOID =*2801758h)
MSVBVM60.DLL:<-kernel32.dll    GetProcAddress+ ->*=*7EED71FCh (HMODULE hModule=750e0000h, struct {LPCSTR lpProcName=*7296EB0Ch->"BlockInput")
MSVBVM60.DLL:->USER32.dll      BlockInput (BOOL fBlockIt=1h)
MSVBVM60.DLL:<-USER32.dll      BlockInput ->int=0h (BOOL fBlockIt=1h)
MSVBVM60.DLL:->USER32.dll      GetKeyboardState (PBYTE lpKeyState=*18D904h->'0')
MSVBVM60.DLL:<-USER32.dll      GetKeyboardState ->int=1h (PBYTE lpKeyState=*18D904h->'\00h')
MSVBVM60.DLL:->USER32.dll      SendInput (UINT cInputs=1h, *pInputs=*18DA04h->struct {DWORD type=1h}, int cbSize=1Ch)
MSVBVM60.DLL:<-USER32.dll      SendInput ->unsigned int=1h (UINT cInputs=1h, *pInputs=*18DA04h->struct {DWORD type=1h}, int cbSize=1Ch)
MSVBVM60.DLL:->USER32.dll      SendInput (UINT cInputs=1h, *pInputs=*18DA04h->struct {DWORD type=1h}, int cbSize=1Ch)
MSVBVM60.DLL:<-USER32.dll      SendInput ->unsigned int=1h (UINT cInputs=1h, *pInputs=*18DA04h->struct {DWORD type=1h}, int cbSize=1Ch)
MSVBVM60.DLL:->USER32.dll      GetKeyboardState (PBYTE lpKeyState=*18D804h->'5')
MSVBVM60.DLL:<-USER32.dll      GetKeyboardState ->int=1h (PBYTE lpKeyState=*18D804h->'\00h')