Link to home
Start Free TrialLog in
Avatar of romieb69
romieb69Flag for United States of America

asked on

How to determine 3rd party command button state

Good afternoon experts! I have written a low level keyboard hook that is specifically looking to trap the key down event of the enter key. What I'm looking to do is to find out if a particular OK button has focus when the enter key is pressed. I have the windows handle of the OK button that I'm monitoring.

It seems at some point I read something about an API call out there that could find out the state of a command button but can not find what I'm looking for.

Any suggestions are greatly appreciated!  I've attached the code that I have written so far.

Thanks!!!

R
Imports System.Runtime.InteropServices
Imports System.Drawing
Imports System.Reflection
Imports System.Threading
Public Class clsKeyboardHook

    Public Declare Function UnhookWindowsHookEx Lib "user32" _
      (ByVal hHook As Integer) As Integer

    Public Declare Function SetWindowsHookEx Lib "user32" _
      Alias "SetWindowsHookExA" (ByVal idHook As Integer, _
      ByVal lpfn As KeyboardHookDelegate, ByVal hmod As Integer, _
      ByVal dwThreadId As Integer) As Integer

    Private Declare Function GetAsyncKeyState Lib "user32" _
      (ByVal vKey As Integer) As Integer

    Private Declare Function CallNextHookEx Lib "user32" _
      (ByVal hHook As Integer, _
      ByVal nCode As Integer, _
      ByVal wParam As Integer, _
      ByVal lParam As KBDLLHOOKSTRUCT) As Integer

    Public Structure KBDLLHOOKSTRUCT
        Public vkCode As Integer
        Public scanCode As Integer
        Public flags As Integer
        Public time As Integer
        Public dwExtraInfo As Integer
    End Structure

    ' Low-Level Keyboard Constants
    Private Const HC_ACTION As Integer = 0
    Private Const LLKHF_EXTENDED As Integer = &H1
    Private Const LLKHF_INJECTED As Integer = &H10
    Private Const LLKHF_DOWN As Integer = &H81

    ' Virtual Keys
    Public Const VK_ENTER = &HD
    Private Const WH_KEYBOARD_LL As Integer = 13&
    Public KeyboardHandle As Integer
    Private k_btnClicked As Boolean = False
    Public Property BtnClicked() As Boolean
        Get
            Return k_btnClicked
        End Get
        Set(ByVal value As Boolean)
            k_btnClicked = value
        End Set
    End Property


    Public Function IsHooked( _
      ByRef Hookstruct As KBDLLHOOKSTRUCT) As Boolean
        If (Hookstruct.vkCode = VK_ENTER) Then
            Return True
        End If
        'Note: you can add any key to this routine if you need to hook something other than enter
        Return False
    End Function

    
    Public Function KeyboardCallback(ByVal Code As Integer, _
      ByVal wParam As Integer, _
      ByRef lParam As KBDLLHOOKSTRUCT) As Integer

        If (Code = HC_ACTION) Then
            If (IsHooked(lParam)) Then 'is hooked will only be true on enter key

                'Here I determine if the enter key down event took place on the OK button I'm intercepting

                BtnClicked = True
                Return 1
            End If
        End If
        Return CallNextHookEx(KeyboardHandle, _
          Code, wParam, lParam)

    End Function


    Public Delegate Function KeyboardHookDelegate( _
      ByVal Code As Integer, _
      ByVal wParam As Integer, ByRef lParam As KBDLLHOOKSTRUCT) _
                   As Integer

    <MarshalAs(UnmanagedType.FunctionPtr)> _
    Private callback As KeyboardHookDelegate

    Public Function HookKeyboard(ByVal btnHndl As Integer) As Boolean
        callback = New KeyboardHookDelegate(AddressOf KeyboardCallback)

        KeyboardHandle = SetWindowsHookEx( _
          WH_KEYBOARD_LL, callback, _
          Marshal.GetHINSTANCE( _
          [Assembly].GetExecutingAssembly.GetModules()(0)).ToInt32, 0)

        If CheckHooked() Then
            Return True
        Else
            Return False
        End If
    End Function

    Public Function CheckHooked() As Boolean
        If Not (Hooked()) Then
            Throw New Exception("Could not set keyboard hook")
            Return False
        Else
            Return True
        End If
    End Function

    Private Function Hooked()
        Hooked = KeyboardHandle <> 0
    End Function

    Public Sub UnhookKeyboard()
        If (Hooked()) Then
            Call UnhookWindowsHookEx(KeyboardHandle)
        End If
    End Sub



End Class

Open in new window

ASKER CERTIFIED SOLUTION
Avatar of Mike Tomlinson
Mike Tomlinson
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of romieb69

ASKER

Yeah... I see what you mean. I'm already using GetForegroundWindow so I'm good there and have the target window in focus.

Isn't there something like GetButtonState or something similar that I can provide the winId for that button to and find out what it's various properties are? Or is that kind of functionality limited to my own app and not necessairly a button running in an external application?
Alrighty then...try using SendMessage() with BM_GETSTATE and check for a return value of BST_FOCUS:
http://msdn.microsoft.com/en-us/library/bb775988(VS.85).aspx


=)
Aaahhh yeah good idea. I'll give it a shot and see if I can make that do it on Monday. Thanks!!
Hey IM ...

I'm just now getting a chance to play with this again. I've given up on SendKeys... you can find out what a button's state is (depressed etc) through this avenue but not whether or not it has focus. So I've gone back to the first suggestion you have made.

Not quite making it work and I suspect I'm calling AttachThreadInput incorrectly.

On attempt number 1 I tried GetWindowThreadProcessId(winHndl, intPID)  where winHndl is the actual window that contains the button.  On attempt number 2 I tried GetWindowThreadProcessId(btnHndl, intPID) where btnHndl is the actual button being monitored.

I'm missing something ...the boolean blnAttach is always false.
Class...
    <DllImport("user32", CharSet:=CharSet.Ansi, SetLastError:=True, ExactSpelling:=True)> _
    Public Shared Function AttachThreadInput(ByVal idAttach As Integer, ByVal idAttachTo As Integer, ByVal fAttach As Integer) As Integer
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
    Public Shared Function GetWindowThreadProcessId(ByVal handle As IntPtr, ByRef processId As Integer) As Integer
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, ExactSpelling:=True)> _
    Public Shared Function GetFocus() As IntPtr
    End Function


Public Function KeyboardCallback(ByVal Code As Integer, _
      ByVal wParam As Integer, _
      ByRef lParam As KBDLLHOOKSTRUCT) As Integer
        Dim ptrOK As IntPtr
        Dim intPID As Integer
        Dim blnAttach As Boolean = False
        Dim thCurThread As Thread

        If (Code = HC_ACTION) Then
            If (IsHooked(lParam)) Then 'is hooked will only be true on enter key
                thCurThread = Thread.CurrentThread()

                'Here I determine if the enter key down event took place on the OK button I'm intercepting
                'GetWindowThreadProcessId(winHndl, intPID)
                GetWindowThreadProcessId(btnHndl, intPID)
                AttachThreadInput(thCurThread.ManagedThreadId, intPID, blnAttach)
                If blnAttach Then
                    ptrOK = GetFocus()
                    If ptrOK.ToInt32 = btnHndl Then
                        BtnClicked = True
                        btnHndl = 0
                        Return 1
                    End If
                End If
            End If
        End If
        Return CallNextHookEx(KeyboardHandle, _
          Code, wParam, lParam)
If (Code = HC_ACTION) Then
            If (IsHooked(lParam)) Then 'is hooked will only be true on enter key
                thCurThread = Thread.CurrentThread()

                'Here I determine if the enter key down event took place on the OK button I'm intercepting
                'GetWindowThreadProcessId(winHndl, intPID)
                GetWindowThreadProcessId(btnHndl, intPID)
                AttachThreadInput(thCurThread.ManagedThreadId, intPID, blnAttach)
                If blnAttach Then
                    ptrOK = GetFocus()
                    If ptrOK.ToInt32 = btnHndl Then
                        BtnClicked = True
                        btnHndl = 0
                        Return 1
                    End If
                End If
            End If
        End If
        Return CallNextHookEx(KeyboardHandle, _
          Code, wParam, lParam)

    End Function

Open in new window

Hmmm...haven't used it in awhile.  You need the PID for your thread and the PID for the external thread.

Next you use AttachThreadInput() to attach your thread to the external thread.

Something like:

                Dim ThreadID1 As Integer = GetWindowThreadProcessId(Me.Handle, 0) ' <-- Our PID
                Dim ThreadID2 As Integer = GetWindowThreadProcessId(btnHndl, 0) ' <-- External PID
                Call AttachThreadInput(ThreadID1, ThreadID2, True) ' <-- Attach

                ' ... use GetFocus() in here ...

                Call AttachThreadInput(ThreadID1, ThreadID2, False) ' <-- Unattach

What kind of app are you using though?  I've used "Me.Handle" which assumes a WinForms app and that the code is running in the context of the Form.  You may need to pass in a form/control into your class or we can figure out a different route to get your PID (possibly through Process class).
This code is a class in a DLL that is called by a win form app when I need to activate my keyboard hook delegate...

So I was thinking that something like this:

Dim thCurThread As Thread

thCurThread = Thread.CurrentThread()   'set thCurThread to the current thread
intMyPID = thCurThread.ManagedThreadId 'obtain the current threads ID

AttachThreadInput(intMyPID, intBtnPID, blnAttach)

Interestingly enough intMyPID is always 10 ...

Also I read somewhere that the first parameter of AttachThreadInput can't be a system thread. I wonder if that is what I'm actually providing it...

I've attached all of the code in the class thus far.
Imports System.Runtime.InteropServices
Imports System.Drawing
Imports System.Reflection
Imports System.Threading
Public Class clsKeyboardHook
    <DllImport("user32", CharSet:=CharSet.Ansi, SetLastError:=True, ExactSpelling:=True)> _
    Public Shared Function AttachThreadInput(ByVal idAttach As Integer, ByVal idAttachTo As Integer, ByVal fAttach As Integer) As Integer
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
    Public Shared Function GetWindowThreadProcessId(ByVal handle As IntPtr, ByRef processId As Integer) As Integer
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, ExactSpelling:=True)> _
    Public Shared Function GetFocus() As IntPtr
    End Function

    Public Declare Function UnhookWindowsHookEx Lib "user32" _
      (ByVal hHook As Integer) As Integer

    Public Declare Function SetWindowsHookEx Lib "user32" _
      Alias "SetWindowsHookExA" (ByVal idHook As Integer, _
      ByVal lpfn As KeyboardHookDelegate, ByVal hmod As Integer, _
      ByVal dwThreadId As Integer) As Integer

    Private Declare Function GetAsyncKeyState Lib "user32" _
      (ByVal vKey As Integer) As Integer

    Private Declare Function CallNextHookEx Lib "user32" _
      (ByVal hHook As Integer, _
      ByVal nCode As Integer, _
      ByVal wParam As Integer, _
      ByVal lParam As KBDLLHOOKSTRUCT) As Integer

    Public Structure KBDLLHOOKSTRUCT
        Public vkCode As Integer
        Public scanCode As Integer
        Public flags As Integer
        Public time As Integer
        Public dwExtraInfo As Integer
    End Structure

    ' Low-Level Keyboard Constants
    Private Const HC_ACTION As Integer = 0
    Private Const LLKHF_EXTENDED As Integer = &H1
    Private Const LLKHF_INJECTED As Integer = &H10
    Private Const LLKHF_DOWN As Integer = &H81

    ' Virtual Keys
    Public Const VK_ENTER = &HD
    Private Const WH_KEYBOARD_LL As Integer = 13&
    Public KeyboardHandle As Integer
    Public btnHndl As Integer
    Public winHndl As Integer
    Private k_btnClicked As Boolean = False
    Public Property BtnClicked() As Boolean
        Get
            Return k_btnClicked
        End Get
        Set(ByVal value As Boolean)
            k_btnClicked = value
        End Set
    End Property


    Public Function IsHooked( _
      ByRef Hookstruct As KBDLLHOOKSTRUCT) As Boolean
        If (Hookstruct.vkCode = VK_ENTER) Then
            Return True
        End If
        'Note: you can add any key to this routine if you need to hook something other than enter
        Return False
    End Function


    Public Function KeyboardCallback(ByVal Code As Integer, _
      ByVal wParam As Integer, _
      ByRef lParam As KBDLLHOOKSTRUCT) As Integer
        Dim ptrOK As IntPtr
        Dim intMyPID As Integer = 0
        Dim intBtnPID As Integer = GetWindowThreadProcessId(btnHndl, 0)
        Dim blnAttach As Boolean = False
        Dim thCurThread As Thread

        If (Code = HC_ACTION) Then
            If (IsHooked(lParam)) Then 'is hooked will only be true on enter key
                thCurThread = Thread.CurrentThread()
                intMyPID = thCurThread.ManagedThreadId

                'Here I determine if the enter key down event took place on the OK button I'm intercepting
                AttachThreadInput(intMyPID, intBtnPID, blnAttach)
                If blnAttach Then
                    ptrOK = GetFocus()
                    If ptrOK.ToInt32 = btnHndl Then
                        BtnClicked = True
                        btnHndl = 0
                        Return 1
                    End If
                End If
            End If
        End If
        Return CallNextHookEx(KeyboardHandle, _
          Code, wParam, lParam)

    End Function


    Public Delegate Function KeyboardHookDelegate( _
      ByVal Code As Integer, _
      ByVal wParam As Integer, ByRef lParam As KBDLLHOOKSTRUCT) _
                   As Integer

    <MarshalAs(UnmanagedType.FunctionPtr)> _
    Private callback As KeyboardHookDelegate

    Public Function HookKeyboard(ByVal btnHndlPassIn As Integer, ByVal winHndlInsuranceScr As Integer) As Boolean
        callback = New KeyboardHookDelegate(AddressOf KeyboardCallback)
        btnHndl = btnHndlPassIn
        winHndl = winHndlInsuranceScr
        KeyboardHandle = SetWindowsHookEx( _
          WH_KEYBOARD_LL, callback, _
          Marshal.GetHINSTANCE( _
          [Assembly].GetExecutingAssembly.GetModules()(0)).ToInt32, 0)

        If CheckHooked() Then
            Return True
        Else
            Return False
        End If
    End Function

    Public Function CheckHooked() As Boolean
        If Not (Hooked()) Then
            Throw New Exception("Could not set keyboard hook")
            Return False
        Else
            Return True
        End If
    End Function

    Private Function Hooked()
        Hooked = KeyboardHandle <> 0
    End Function

    Public Sub UnhookKeyboard()
        If (Hooked()) Then
            Call UnhookWindowsHookEx(KeyboardHandle)
        End If
    End Sub



End Class

Open in new window

I'm not that good at the lower level stuff...I think you need ID() instead of ManagedThreadID():

    intMyPID = Thread.CurrentThread.ID
This solution got it done!

Idle Mind thanks for an excellent suggestion!!
Here's the mistakes I made in the last version of this

1. I tried to use this to set intMyPID
'thCurThread = Thread.CurrentThread()
'intMyPID = thCurThread.ManagedThreadId

What I needed to use was this      

intMyPID = AppDomain.GetCurrentThreadId


But it generates the following warning

Public Shared Function GetCurrentThreadId() As Integer' is obsolete: 'AppDomain.GetCurrentThreadId has been deprecated because it does not provide a stable Id when managed threads are running on fibers (aka lightweight threads). To get a stable identifier for a managed thread, use the ManagedThreadId property on Thread.  http://go.microsoft.com/fwlink/?linkid=14202'.

2. I was attaching to the btn pid incorrectly... this is the correct way:
AttachThreadInput(intMyPID, intBtnPID, True) with the third parameter being a boolean toggle

3. This one killed me and took forever to think to pop in there before calling GetFocus()
SetForegroundWindow(winHndl)

Thanks again IM for the great suggestion and all the guidance.
Imports System.Runtime.InteropServices
Imports System.Drawing
Imports System.Reflection
Imports System.Threading
Public Class clsKeyboardHook
    <DllImport("user32", CharSet:=CharSet.Ansi, SetLastError:=True, ExactSpelling:=True)> _
    Public Shared Function AttachThreadInput(ByVal idAttach As Integer, ByVal idAttachTo As Integer, ByVal fAttach As Integer) As Integer
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
    Public Shared Function GetWindowThreadProcessId(ByVal handle As IntPtr, ByRef processId As Integer) As Integer
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, ExactSpelling:=True)> _
    Public Shared Function GetFocus() As IntPtr
    End Function

    Public Declare Function UnhookWindowsHookEx Lib "user32" _
      (ByVal hHook As Integer) As Integer

    Public Declare Function SetWindowsHookEx Lib "user32" _
      Alias "SetWindowsHookExA" (ByVal idHook As Integer, _
      ByVal lpfn As KeyboardHookDelegate, ByVal hmod As Integer, _
      ByVal dwThreadId As Integer) As Integer

    Private Declare Function GetAsyncKeyState Lib "user32" _
      (ByVal vKey As Integer) As Integer

    Private Declare Function CallNextHookEx Lib "user32" _
      (ByVal hHook As Integer, _
      ByVal nCode As Integer, _
      ByVal wParam As Integer, _
      ByVal lParam As KBDLLHOOKSTRUCT) As Integer

    Public Structure KBDLLHOOKSTRUCT
        Public vkCode As Integer
        Public scanCode As Integer
        Public flags As Integer
        Public time As Integer
        Public dwExtraInfo As Integer
    End Structure

    ' Low-Level Keyboard Constants
    Private Const HC_ACTION As Integer = 0
    Private Const LLKHF_EXTENDED As Integer = &H1
    Private Const LLKHF_INJECTED As Integer = &H10
    Private Const LLKHF_DOWN As Integer = &H81

    ' Virtual Keys
    Public Const VK_ENTER = &HD
    Private Const WH_KEYBOARD_LL As Integer = 13&
    Public KeyboardHandle As Integer
    Public btnHndl As Integer
    Public winHndl As Integer
    Public intMyPID As Integer = 0
    Public intBtnPID As Integer
    Private k_btnClicked As Boolean = False
    Public Property BtnClicked() As Boolean
        Get
            Return k_btnClicked
        End Get
        Set(ByVal value As Boolean)
            k_btnClicked = value
        End Set
    End Property


    Public Function IsHooked( _
      ByRef Hookstruct As KBDLLHOOKSTRUCT) As Boolean
        If (Hookstruct.vkCode = VK_ENTER) Then
            Return True
        End If
        'Note: you can add any key to this routine if you need to hook something other than enter
        Return False
    End Function


    Public Function KeyboardCallback(ByVal Code As Integer, _
      ByVal wParam As Integer, _
      ByRef lParam As KBDLLHOOKSTRUCT) As Integer
        Dim ptrOK As IntPtr
        If (Code = HC_ACTION) Then
            If (IsHooked(lParam)) Then 'is hooked will only be true on enter key
                'thCurThread = Thread.CurrentThread()
                'intMyPID = thCurThread.ManagedThreadId
                'intMyPID = AppDomain.GetCurrentThreadId
                'Here I determine if the enter key down event took place on the OK button I'm intercepting
                AttachThreadInput(intMyPID, intBtnPID, True)
                SetForegroundWindow(winHndl)
                ptrOK = GetFocus()
                If ptrOK.ToInt32 = btnHndl Then
                    BtnClicked = True
                    btnHndl = 0
                    blnAttach = False
                    AttachThreadInput(intMyPID, intBtnPID, False)
                    Return 1

                End If
            End If
        End If
        AttachThreadInput(intMyPID, intBtnPID, False)
        Return CallNextHookEx(KeyboardHandle, _
          Code, wParam, lParam)

    End Function


    Public Delegate Function KeyboardHookDelegate( _
      ByVal Code As Integer, _
      ByVal wParam As Integer, ByRef lParam As KBDLLHOOKSTRUCT) _
                   As Integer

    <MarshalAs(UnmanagedType.FunctionPtr)> _
    Private callback As KeyboardHookDelegate

    Public Function HookKeyboard(ByVal btnHndlPassIn As Integer, ByVal winHndlInsuranceScr As Integer) As Boolean
        Dim thCurThread As Thread

        callback = New KeyboardHookDelegate(AddressOf KeyboardCallback)
        btnHndl = btnHndlPassIn
        intBtnPID = GetWindowThreadProcessId(btnHndl, 0)
        winHndl = winHndlInsuranceScr
        KeyboardHandle = SetWindowsHookEx( _
          WH_KEYBOARD_LL, callback, _
          Marshal.GetHINSTANCE( _
          [Assembly].GetExecutingAssembly.GetModules()(0)).ToInt32, 0)

        If CheckHooked() Then
            intMyPID = AppDomain.GetCurrentThreadId
            thCurThread = Thread.CurrentThread()
            'AttachThreadInput(intMyPID, intBtnPID, True)
            Return True
        Else
            Return False
        End If
    End Function

    Public Function CheckHooked() As Boolean
        If Not (Hooked()) Then
            Throw New Exception("Could not set keyboard hook")
            Return False
        Else
            Return True
        End If
    End Function

    Private Function Hooked()
        Hooked = KeyboardHandle <> 0
    End Function

    Public Sub UnhookKeyboard()
        If (Hooked()) Then
            Call UnhookWindowsHookEx(KeyboardHandle)
        End If
    End Sub



End Class

Open in new window

Yay!...glad you were able to hammer something together.  =)