自作WindowsAPIクラスにウインドウのクラス名を返すFunctionを追加した

ウインドウのクラス名を返すFunction

WinAPIの勉強中。

f:id:akashi_keirin:20190105203721j:plain

コチラの本に、アプリケーション別のクラス名が掲載されていたのだが、Internet Explorerのクラス名が載っていなかったので、アプリケーションのクラス名を返すFunctionを作ってみた。

コードを書いてから実行する中で、何箇所か致命的なタイプミスがあって、何度かExcelが強制終了したのだが、大丈夫なのだろうか……。

もちろん、Win32API関数を使います。

家のPCは64bitなんだけれど、職場のPCが32bitなので……。

使用するWinAPI関数

次の関数を使った。

  1. FindWindow関数
  2. GetNextWindow関数
  3. IsWindowVisible関数
  4. GetWindowText関数
  5. GetClassName関数

以上五つ。

例によって、自作のWindowsAPIクラスに組み込んだ。

WindowsAPIクラスへの追加

追加するものが多いので、順に挙げていく。

リスト1 クラスモジュール宣言セクション
Private Declare Function FindWindow Lib "user32" _
                  Alias "FindWindowA" (ByVal lpClassName As String, _
                                       ByVal lpWindowName As String) As Long
Private Declare Function GetNextWindow Lib "user32" _
                  Alias "GetWindow" (ByVal hwnd As Long, _
                                     ByVal wFlag As Long) As Long
Private Declare Function IsWindowVisible Lib "user32" ( _
                                         ByVal hwnd As Long) As Long
Private Declare Function GetWindowText Lib "user32" _
                  Alias "GetWindowTextA" (ByVal hwnd As Long, _
                                          ByVal lpString As String, _
                                          ByVal cch As Long) As Long
Private Declare Function GetClassName Lib "user32" _
                  Alias "GetClassNameA" (ByVal hwnd As Long, _
                                         ByVal lpClassName As String, _
                                         ByVal nMaxCount As Long) As Long

まずは、Win32APIの関数群。

最初、あろうことかGetClassName関数の返り値の型をなぜか「String」と打ち間違えていたために、コード実行後にExcelが強制終了したのだが、大丈夫だったのだろうか……。コワーーー。

スト2 クラスモジュール宣言セクション
Private Const GW_HWNDLAST As Long = 1
Private Const GW_HWNDNEXT As Long = 2

これらは、GetNextWindow関数の引数(第2引数wFlag)にするための定数。

どうも、

hwnd = GetNextWindow(hwnd, GW_HWNDLAST)

とすれば、最後に取得するウインドウである場合にGetNextWindow関数がウインドウハンドル(hwndの値)と同じ値を返すっぽい。

それによって終了判定に使うことができるらしい。ふーん。

リスト3 クラスモジュール
Public Function getWindowClassName(ByVal targetAppNameKeyWord As String) As String
  Dim gotClassName As String * 100    '……(1)'
  Dim gotCaption As String * 200
  Dim hwnd As Long
  hwnd = FindWindow(vbNullString, vbNullString)    '……(2)'
  Do
    If IsWindowVisible(hwnd) Then    '……(3)'
      Call GetWindowText(hwnd, gotCaption, Len(gotCaption))
      If InStr(1, gotCaption, targetAppNameKeyWord) > 0 Then    '……(4)'
        Call GetClassName(hwnd, gotClassName, Len(gotClassName))
        Dim ret As String
        ret = Left(gotClassName, InStr(gotClassName, vbNullChar) - 1)    '……(5)'
        Exit Do
      End If
    End If
    hwnd = GetNextWindow(hwnd, GW_HWNDNEXT)    '……(6)'
  Loop Until hwnd = GetNextWindow(hwnd, GW_HWNDLAST)    '……(7)'
  getWindowClassName = ret
End Function

これがメインのコード。

たぶん、気をつけないといけないのは、(1)の

Dim gotClassName As String * 100
Dim gotCaption As String * 200

String型変数を宣言する際に固定長にしておくこと。

固定長にして、使用するメモリのサイズを厳密に確保しておかないと、えらいことになるような気がする。このあたり、C言語に詳しい人がいたら、教えろ教えてください。

(2)の

hwnd = FindWindow(vbNullString, vbNullString)

は、FindWindow関数を用いて、ウインドウハンドルを取得。引数に二つともvbNullStringを渡しているので、とりあえずテキトーにどれか一つを取得しているのだと思う。違っていたら教えろ教えてください。

ここからDoループに突入。

(3)からの

If IsWindowVisible(hwnd) Then    '……(3)'
  Call GetWindowText(hwnd, gotCaption, Len(gotCaption))
  If InStr(1, gotCaption, targetAppNameKeyWord) > 0 Then    '……(4)'
    Call GetClassName(hwnd, gotClassName, Len(gotClassName))
    Dim ret As String
    ret = Left(gotClassName, InStr(gotClassName, vbNullChar) - 1)    '……(5)'
    Exit Do
  End If
End If
hwnd = GetNextWindow(hwnd, GW_HWNDNEXT)    '……(6)'

If文がネストしているので、読みづらいかも知れない。

まず、(3)でIsWindowVisible関数を用いて、可視状態のウインドウかどうかを調べる。

この関門をクリアすると、今度はGetWindowTextでウインドウのキャプションを取得する。

Functionのくせに返り値を受け取る形にしないのがキモチワルイけれど、ステップ実行してみると、

Call GetWindowText(hwnd, gotCaption, Len(gotCaption))

の実行直後に「gotCaption」にウィンドウのキャプションと残りの文字数をNull文字で埋めた固定長の文字列が格納されていることがわかる。ここの返り値が結構長い文字列になることがあるので、とりあえず固定長を200と大きめに取っておいたが、これが適切なのかどうかもよくわからない。これまた詳しい人がいたら教えろ教えてください。

次に、(4)の

If InStr(1, gotCaption, targetAppNameKeyWord) > 0 Then

の条件判定。

変数gotCaptionには、ウインドウのキャプションとNull文字が入っているので、引数の「targetAppNameKeyWord」を含んでいるなら、お目当てのウインドウだということで、Ifブロック内に進むようにした。

たいていアプリケーション名が入っていると思うので。

ただ、アプリケーション名が全角文字のときにどうなるのかはわからない。一太郎とか。

このあたりも、詳しい人がいたら、教えろ教えてください。

Ifブロック内に突入したら、まず

Call GetClassName(hwnd, gotClassName, Len(gotClassName))

GetClassNameを呼び出す。

これで、gotClassNameに残りの文字数をNull文字で埋めた固定長の文字列が格納される。

あとは、(5)の

ret = Left(gotClassName, InStr(gotClassName, vbNullChar) - 1)

で正味の文字の部分だけを切り出してretに格納し、ループを抜ける。

ループを抜けたら、

getWindowClassName = ret

retの内容をreturnして終わり。

ループから抜けられなかったら、(6)の

hwnd = GetNextWindow(hwnd, GW_HWNDNEXT)

で次のウインドウハンドルを取得してループの先頭へ。

お目当てのウインドウにぶち当たらずに最後まで行ってしまったら、(7)の

Loop Until hwnd = GetNextWindow(hwnd, GW_HWNDLAST)

によってループを抜ける。この場合は""が返ることになる。

使ってみる

目的は、Internet Explorerのクラス名を知ることなので、次のコードで実行する。

リスト4 標準モジュール
Public Sub test()
  Debug.Print WindowsAPI.getWindowClassName("Internet")
End Sub

アホみたいに簡単。

テキトーにIEを起動してから実行すると、イミディエイト・ウインドウに

f:id:akashi_keirin:20190106182051j:plain

IEFrame」と表示された。

これがInternet Explorerのクラス名ということだ。

おわりに

何かに使えるかも知れないので、「WinAPIEnums」という標準モジュールを挿入し、次のような列挙体を作った。

リスト5 標準モジュール宣言セクション
Public Enum AppClassName
  acNotepad
  acPaint
  acWordPad
  acExcel
  acWord
  acOutlook
  acPowerpoint
  acInternetExplorer
End Enum

その上で、我がWindowsAPIクラスのモジュールに次のPrivateメソッドを追加。

リスト6 クラスモジュール
Private Function getApplicationClassName(ByVal targetApp As AppClassName) As String
  Dim ret As String
  Select Case targetApp
    Case acNotepad:          ret = "Notepad"
    Case acPaint:            ret = "MSPaintApp"
    Case acWordPad:          ret = "WordPadClass"
    Case acExcel:            ret = "XLMAIN"
    Case acWord:             ret = "OpusApp"
    Case acOutlook:          ret = "rctrl_renwnd32"
    Case acPowerpoint:       ret = "PPTFrameClass"
    Case anInternetExplorer: ret = "IEFrame"
    Case Else:               ret = ""
  End Select
  getApplicationClassName = ret
End Function

標準モジュール「WinAPIEnums」と必ずセットでインポートしなければならなくなるけれど、このようにすることで、今後WindowsAPIオブジェクトを利用する際にクラス名を指定しやすくなった。

……とはいえ、列挙されているアプリ以外については、今後追加していかねばなりませんが……。