Real-time KeyHistory / MouseHistory

Post your working scripts, libraries and tools
lexikos
Posts: 6333
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Real-time KeyHistory / MouseHistory

24 Dec 2016, 02:24

These are a couple of old scripts that I use occasionally instead of KeyHistory.

KeyHistoryWindow.ahk

Shows keyboard events similar to KeyHistory, but updates in real-time as you press or release any key.

Sample output:

Code: Select all

48  023     u     0.00  h                 0x0
45  012           0.03  e                 0x0
4C  026           0.09  l                 0x0
45  012     u     0.02  e                 0x0
4C  026     u     0.08  l                 0x0
4C  026           0.05  l                 0x0
4C  026     u     0.09  l                 0x0
4F  018           0.05  o                 0x0
4F  018     u     0.09  o                 0x0
This shows the VK, SC, a/!/u (artificial/alt/key-up), elapsed time, key name and extra info. The extra info is generally 0 unless it was sent by AutoHotkey, in which case the values are as defined in the AutoHotkey source code:

Code: Select all

#define KEY_IGNORE 0xFFC3D44F
#define KEY_PHYS_IGNORE (KEY_IGNORE - 1)  // Same as above but marked as physical for other instances of the hook.
#define KEY_IGNORE_ALL_EXCEPT_MODIFIER (KEY_IGNORE - 2)  // Non-physical and ignored only if it's not a modifier.
...
#define KEY_IGNORE_LEVEL(LEVEL) (KEY_IGNORE_ALL_EXCEPT_MODIFIER - LEVEL)

Code: Select all

; https://autohotkey.com/boards/viewtopic.php?f=6&t=26059

; GUI Legend:
; VK  SC_  _a!u  Elapsed  Key name_________  Extra_info

#NoEnv
#Persistent
#InstallKeybdHook
hHookKeybd := DllCall("SetWindowsHookEx", "int", 13 ; WH_KEYBOARD_LL = 13
    , "ptr", RegisterCallback("Keyboard")
    ; hMod is not required on Win 7, but seems to be required on XP even
    ; though this type of hook is never injected into other processes:
    , "ptr", DllCall("GetModuleHandle", "ptr", 0, "ptr")
    , "uint", 0, "ptr") ; dwThreadId
#KeyHistory(10)

OnMessage(0x201, "WM_LBUTTONDOWN")
OnMessage(0x100, "WM_KEYDOWN")

Gui, +LastFound -DPIScale
WinSet, Transparent, 200
Gui, +ToolWindow +AlwaysOnTop
Gui, Margin, 10, 10
Gui, Font,, Lucida Console
Gui, Add, Text, vKH, 00  000  ____  1000.00  Browser_Favorites 0xFFFFFFFF
GuiControlGet, KH, Pos
GuiControl,, KH  ; clear dummy sizing text
gosub Resize
return

#MaxThreadsBuffer, On
!WheelUp::
!WheelDown::
#MaxThreadsBuffer, Off
    history_size := #KeyHistory() + ((A_ThisHotkey="!WheelUp") ? +1 : -1)
    #KeyHistory(history_size>0 ? history_size : 1)
    ; Delay resize to improve hotkey responsiveness.
    SetTimer, Resize, -10
return

Resize:
    ; Resize label to fit key history.
    gui_h := KHH*(#KeyHistory())
    GuiControl, Move, KH, h%gui_h%
    gui_h += 20

    Gui, +LastFound
    ; Determine visibility.
    WinGet, style, Style
    gui_visible := style & 0x10000000

    ;Gui, Show, % "AutoSize NA " (gui_visible ? "" : "Hide")
    ;** Not used because we need to know the previous height,
    ;   and its simpler to resize manually.
    
    ; Determine current position and height.
    WinGetPos, gui_x, gui_y, , gui_h_old
    ; Use old height to determine if we should reposition, *only when shrinking*.
    ; This way we can move the GUI somewhere else, and the script won't reposition it.
    ;if (gui_h_old < gui_h)
    ;    gui_h_old := gui_h
    ; Determine working area (primary screen size minus taskbar.)
    SysGet, wa_, MonitorWorkArea

    SysGet, twc_h, 51 ; SM_CYSMCAPTION
    SysGet, bdr_h, 8  ; SM_CYFIXEDFRAME
    if (!gui_visible)
    {
        gui_x = 72 ; Initially on the left side.
        gui_y := wa_bottom-(gui_h+twc_h+bdr_h*2+10)
    }
    else
    {   ; Move relative to bottom edge when closer to the bottom.
        if (gui_y+gui_h//2 > (wa_bottom-wa_top)//2)
            gui_y += gui_h_old-(gui_h+twc_h+bdr_h*2)
    }
    Gui, Show, x%gui_x% y%gui_y% h%gui_h% NA, Key History
return


Keyboard(nCode, wParam, lParam) {
    global KeyBuffer
    
    Critical
    
    if KeyHistory(1, vk, sc, flags)
        && NumGet(lParam+0, "uint") = vk
        && NumGet(lParam+4, "uint") = sc
        && NumGet(lParam+8, "uint") = flags
        buf_max := 0 ; Don't show key-repeat.
    else
        buf_max := #KeyHistory()

    if (buf_max > 0)
    {
        ; Push older key events to the back.
        if (buf_max > 1)
            DllCall("RtlMoveMemory", "ptr", &KeyBuffer+16+A_PtrSize, "ptr", &KeyBuffer, "ptr", buf_max*16+A_PtrSize)
        ; Copy current key event to the buffer.
        DllCall("RtlMoveMemory", "ptr", &KeyBuffer, "ptr", lParam, "ptr", 16+A_PtrSize)

        ; "gosub Show" slows down the keyboard hook and causes problems, so use a timer.        
        SetTimer, Show, -10
    }
    
    return DllCall("CallNextHookEx", "ptr", 0, "int", nCode, "ptr", wParam, "ptr", lParam, "ptr")
}

KeyHistory(N, ByRef vk, ByRef sc, ByRef flags:=0, ByRef time:=0, ByRef elapsed:=0, ByRef info:=0)
{
    global KeyBuffer
    if N is not integer
        return false
    buf_max := #KeyHistory()
    if (N < 0)
        N += buf_max + 1
    if (N < 1 or N > buf_max)
        return false
    static sz := 16+A_PtrSize
    vk    := NumGet(KeyBuffer, (N-1)*sz, "uint")
    sc    := NumGet(KeyBuffer, (N-1)*sz+4, "uint")
    flags := NumGet(KeyBuffer, (N-1)*sz+8, "uint")
    time  := NumGet(KeyBuffer, (N-1)*sz+12, "uint")
    info  := NumGet(KeyBuffer, (N-1)*sz+16)
    elapsed := time - ((time2 := NumGet(KeyBuffer, N*sz+12, "uint")) ? time2 : time)
    return (vk or sc)
}

#KeyHistory(NewSize="")
{
    global KeyBuffer
    static sz := 16+A_PtrSize
    ; Get current history length.
    if (NewSize="")
        return (cap:=VarSetCapacity(KeyBuffer)//sz)>0 ? cap-1 : 0
    if (NewSize)
    {
        new_cap := (NewSize+1)*sz
        cap := VarSetCapacity(KeyBuffer)
        if (cap > new_cap)
            cap := new_cap
        VarSetCapacity(old_buffer, cap)
        ; Back up previous history.
        DllCall("RtlMoveMemory", "ptr", &old_buffer, "ptr", &KeyBuffer, "ptr", cap)
        
        ; Set new history length.
        VarSetCapacity(KeyBuffer, 0) ; FORCE SHRINK
        VarSetCapacity(KeyBuffer, new_cap, 0)
        
        ; Restore previous history.
        DllCall("RtlMoveMemory", "ptr", &KeyBuffer, "ptr", &old_buffer, "ptr", cap)
        
        ; (Remember N+1 key events to simplify calculation of the Nth key event's elapsed time.)
        ; Put tick count so the initial key event has a meaningful value for "elapsed".
        NumPut(A_TickCount, KeyBuffer, 12, "uint")
    }
    else
    {   ; Clear history entirely.
        VarSetCapacity(KeyBuffer, 0)
    }
}

GetKeyFlagText(flags)
{
    return ((flags & 0x1) ? "e" : " ") ; LLKHF_EXTENDED
        . ((flags & 0x10) ? "a" : " ") ; LLKHF_INJECTED (artificial)
        . ((flags & 0x20) ? "!" : " ") ; LLKHF_ALTDOWN
        . ((flags & 0x80) ? "u" : " ") ; LLKHF_UP (key up)
}

; Gets readable key name, usually identical to the name in KeyHistory.
GetKeyNameText(vkCode, scanCode, isExtendedKey)
{
    return GetKeyName(format("vk{1:02x}sc{3}{2:02x}", vkCode, scanCode, isExtendedKey))
    /* ; For older versions of AutoHotkey:
    ; My Right Shift key shows as vk161 sc54 isExtendedKey=true.  For some
    ; reason GetKeyNameText only returns a name for it if isExtendedKey=false.
    if vkCode = 161
        return "Right Shift"

    VarSetCapacity(buffer, 32, 0)
    DllCall("GetKeyNameText"
        , "UInt", (scanCode & 0xFF) << 16 | (isExtendedKey ? 1<<24 : 0) ;| 1<<25
        , "Str", buffer
        , "Int", 32)

    return buffer
    */
}

Show:
    SetFormat, FloatFast, .2
    SetFormat, IntegerFast, H
    text =
    buf_size := #KeyHistory()
    Loop, % buf_size
    {
        if (KeyHistory(buf_size-A_Index, vk, sc, flags, time, elapsed, info))
        {
            keytext := GetKeyNameText(vk, sc, flags & 0x1)
            
            if (elapsed < 0)
                elapsed := "#err#"
            else
                dt := elapsed/1000.0
            
            ; AHK-style SC
            sc_a := sc
            if (flags & 1)
                sc_a |= 0x100, flags &= ~1
            sc_a := SubStr("000" SubStr(sc_a, 3), -2)
            vk_a := SubStr(vk+0, 3)
            if (StrLen(vk_a)<2)
                vk_a = 0%vk_a%
            StringUpper, vk_a, vk_a
            StringUpper, sc_a, sc_a
            
            flags := GetKeyFlagText(flags & ~0x1)
            
            text .= vk_a "  " sc_a "  " flags "  " SubStr("      " dt, -6) "  "
                . SubStr(keytext "                ", 1, 17) " " info "`n"
        }
    }
    GuiControl,, KH, % text
Return

GuiClose:
ExitApp

WM_KEYDOWN()
{
    if A_Gui
        return true
}

WM_LBUTTONDOWN(wParam, lParam)
{
    global text
    StringReplace, Clipboard, text, `n, `r`n, All
}
MouseHistoryWindow.ahk

Shows mouse events in real-time.

If MERGE_MOVE is true and the last event is a mouse-move, it is replaced. Otherwise there will be one row for each mouse-move event, and events will quickly get pushed off the window while the mouse moves.

Sample output:

Code: Select all

            190    991         2.02
  Middle   1075    548    Up   0.08
  Middle   1075    548  Down   1.70
  WD       1090    427   120   0.39
  Left     1110    391    Up   0.09
  Left     1110    392  Down   0.52
  WU       1111    408   120   1.11
* Right    1105    527    Up   0.00
* Right    1105    527  Down   0.02
  Right    1105    527    Up   0.06
This shows the button name, x and y coordinates, click count/wheel delta/up/down and elapsed time. An asterisk at the beginning of a row indicates that the event is artificial.

Code: Select all

; https://autohotkey.com/boards/viewtopic.php?f=6&t=26059

; If the most recent event is a mouse-move and the mouse moves again,
; enable this to update it instead of adding another mouse-move event.
MERGE_MOVE := true

#NoEnv
#Persistent
#MouseHistory(10)

Gui, +LastFound -DPIScale
WinSet, Transparent, 200
Gui, +ToolWindow +AlwaysOnTop
Gui, Margin, 10, 10
Gui, Font,, Lucida Console
Gui, Add, Text, vMH, .                                 .
GuiControlGet, MH, Pos
GuiControl,, MH  ; clear dummy sizing text
gosub Resize
OnMessage(0x201, "WM_LBUTTONDOWN")
return

#MaxThreadsBuffer, On
!WheelUp::
!WheelDown::
#MaxThreadsBuffer, Off
    history_size := #MouseHistory() + ((A_ThisHotkey="!WheelUp") ? +1 : -1)
    #MouseHistory(history_size>0 ? history_size : 1)
    ; Delay resize to improve hotkey responsiveness.
    SetTimer, Resize, -10
return

Resize:
    ; Resize label to fit mouse history.
    gui_h := MHH*(#MouseHistory())
    GuiControl, Move, MH, h%gui_h%
    gui_h += 20

    Gui, +LastFound
    ; Determine visibility.
    WinGet, style, Style
    gui_visible := style & 0x10000000
    
    ; Determine current position and height.
    WinGetPos, gui_x, gui_y, , gui_h_old
    ; Use old height to determine if we should reposition, *only when shrinking*.
    ; This way we can move the GUI somewhere else, and the script won't reposition it.
    ;if (gui_h_old < gui_h)
    ;    gui_h_old := gui_h
    ; Determine working area (primary screen size minus taskbar.)
    SysGet, wa_, MonitorWorkArea

    SysGet, twc_h, 51 ; SM_CYSMCAPTION
    SysGet, bdr_h, 8  ; SM_CYFIXEDFRAME
    if (!gui_visible)
    {
        gui_x = 10 ; Initially on the left side.
        gui_y := wa_bottom-(gui_h+twc_h+bdr_h*2+10)
    }
    else
    {   ; Move relative to bottom edge when closer to the bottom.
        if (gui_y+gui_h//2 > (wa_bottom-wa_top)//2)
            gui_y += gui_h_old-(gui_h+twc_h+bdr_h*2)
    }
    Gui, Show, x%gui_x% y%gui_y% h%gui_h% NA, Mouse History
return

Show:
    SetFormat, FloatFast, .2
    text =
    buf_size := #MouseHistory()
    Loop, % buf_size
    {
        SetFormat, IntegerFast, D
        
        if MouseHistory(A_Index, msg, x, y, mouseData, flags, time, elapsed)
        {
            SetFormat, IntegerFast, H
            msg := (msg + 0) ""
            SetFormat, IntegerFast, D
            
            ; WM_LBUTTONDOWN/UP/DBLCLK, WM_NC..
            if msg in 0x201,0x202,0x203,0xA1,0xA2,0xA3
                btn = Left
            ; WM_RBUTTONDOWN/UP/DBLCLK, WM_NC..
            else if msg in 0x204,0x205,0x206,0xA4,0xA5,0xA6
                btn = Right
            ; WM_MBUTTONDOWN/UP/DBLCLK, WM_NC..
            else if msg in 0x207,0x208,0x209,0xA7,0xA8,0xA9
                btn = Middle
            ; WM_XBUTTONDOWN/UP/DBLCLK, WM_NC..
            else if msg in 0x20B,0x20C,0x20D,0xAB,0xAC,0xAD
                btn := (mouseData & 0x10000) ? "X1" : "X2"
            ; WM_MOUSEWHEEL
            else if msg = 0x20A
            {
                mouseData := mouseData << 32 >> 48
                btn := (mouseData < 0) ? "WD" : "WU"
            }
            ; WM_MOUSEHWHEEL
            else if msg = 0x20E
            {
                mouseData := mouseData << 32 >> 48
                btn := (mouseData < 0) ? "WL" : "WR"
            }
            ; WM_MOUSEMOVE
            else if msg = 0x200
                btn =
            ; ???
            else btn := msg
            
            clickCount =
            
            ; WM_LBUTTONDBLCLK, WM_NC.., ..R/M/XBUTTONDBLCLK..
            if msg in 0x203,0xA3,0x206,0xA6,0x209,0xA9,0x20D,0xAD
            {
                clickCount := 2
            }
            ; WM_MOUSEWHEEL, WM_MOUSEHWHEEL
            else if msg in 0x20A,0x20E
            {
                clickCount := Abs(mouseData)
                if !clickCount
                    clickCount =
            }
            ; WM_L/R/M/XBUTTONDOWN, WM_NC..
            else if msg in 0x201,0x204,0x207,0x20B,0xA1,0xA4,0xA7,0xAB
            {
                clickCount = Down
            }
            ; WM_L/R/M/XBUTTONUP, WM_NC..
            else if msg in 0x202,0x205,0x208,0x20C,0xA2,0xA5,0xA8,0xAC
            {
                clickCount = Up
            }

            text .= ((flags & 1) ? "* " : "  ")
                 ;.  SubStr(msg "      ", 1, 6)
                 .  SubStr(btn "        ", 1, 8)
                 .  SubStr("    " x, -4) "  " SubStr("    " y, -4)
                 .  SubStr("      " clickCount, -5)
                 .  SubStr("      " elapsed/1000.0, -6) "`n"
        }
        else break
    }
    GuiControl,, MH, % text
Return

GuiClose:
ExitApp


MouseHistory(N, ByRef msg, ByRef x, ByRef y, ByRef mouseData, ByRef flags, ByRef time, ByRef elapsed=-1)
{
    global MouseBuffer
    if N is not integer
        return false
    buf_max := #MouseHistory()
    if (N < 1 or N > buf_max)
        return false
    x           := NumGet(MouseBuffer, ofs:=(N-1)*24, "int") 
    y           := NumGet(MouseBuffer, ofs+4, "int")
    mouseData   := NumGet(MouseBuffer, ofs+8, "uint")
    flags       := NumGet(MouseBuffer, ofs+12, "uint")
    time        := NumGet(MouseBuffer, ofs+16, "uint")
    msg         := NumGet(MouseBuffer, ofs+20, "uint")
    elapsed := time - ((time2 := NumGet(MouseBuffer, N*24+16, "uint")) ? time2 : time)
    return !!msg
}

#MouseHistory(NewSize="")
{
    global MouseBuffer
    static MouseHook, MouseHookProc

    if NewSize =    ; Get current history length.
        return (cap:=VarSetCapacity(MouseBuffer)//24)>0 ? cap-1 : 0

    if NewSize
    {
        if !MouseHook
        {   ; Register the mouse hook.
            MouseHookProc := RegisterCallback("Mouse")
            MouseHook := DllCall("SetWindowsHookEx", "int", 14, "ptr", MouseHookProc, "uint", 0, "uint", 0, "ptr")
        }
        
        new_cap := (NewSize+1)*24 ; sizeof(MSLLHOOKSTRUCT)=24
        cap := VarSetCapacity(MouseBuffer)
        if (cap > new_cap)
            cap := new_cap
        VarSetCapacity(old_buffer, cap)
        ; Back up previous history.
        DllCall("RtlMoveMemory", "ptr", &old_buffer, "ptr", &MouseBuffer, "ptr", cap)
        
        ; Set new history length.
        VarSetCapacity(MouseBuffer, 0) ; FORCE SHRINK
        VarSetCapacity(MouseBuffer, new_cap, 0)
        
        ; Restore previous history.
        DllCall("RtlMoveMemory", "ptr", &MouseBuffer, "ptr", &old_buffer, "ptr", cap)
        
        ; (Remember N+1 mouse events to simplify calculation of the Nth mouse event's elapsed time.)
        ; Put tick count so the initial mouse event has a meaningful value for "elapsed".
        NumPut(A_TickCount, MouseBuffer, 16, "uint")
    }
    else
    {
        if MouseHook
        {   ; Unregister the mouse hook.
            DllCall("UnhookWindowsHookEx", "ptr", MouseHook)
            DllCall("GlobalFree", "ptr", MouseHookProc)
            MouseHook =
        }
        ; Clear history entirely.
        VarSetCapacity(MouseBuffer, 0)
    }
}

; Mouse hook callback - records mouse events into MouseBuffer.
Mouse(nCode, wParam, lParam)
{
    global MouseBuffer, MERGE_MOVE
    Critical
    if (buf_max:=#MouseHistory()) > 0
    {
        if MERGE_MOVE && NumGet(MouseBuffer, 20, "uint") = 0x200
        ;if MERGE_MOVE && wParam = 0x200 && NumGet(MouseBuffer, 20, "uint") = 0x200
        {
            ; Update the most recent (mouse-move) event.
            DllCall("RtlMoveMemory", "ptr", &MouseBuffer, "ptr", lParam, "ptr", 20)
        }
        else
        {
            ; Push older mouse events to the back.
            if (buf_max > 1)
                DllCall("RtlMoveMemory", "ptr", &MouseBuffer+24, "ptr", &MouseBuffer, "ptr", buf_max*24)
            ; Copy current mouse event to the buffer.
            DllCall("RtlMoveMemory", "ptr", &MouseBuffer, "ptr", lParam, "ptr", 20)
        }
        NumPut(wParam, MouseBuffer, 20, "uint") ; Put wParam in place of dwEventInfo.
        ; "gosub Show" slows down the mouse hook and causes problems, so use a timer.        
        SetTimer, Show, -10
    }
    return DllCall("CallNextHookEx", "uint", 0, "int", nCode, "ptr", wParam, "ptr", lParam, "ptr")
}

WM_LBUTTONDOWN(wParam, lParam)
{
    global text
    StringReplace, Clipboard, text, `n, `r`n, All
}
Common Features

Clicking the GUI copies its content to the clipboard.

Alt+WheelUp/WheelDown can be used to change the number of events that are displayed. I typically only use one of these scripts at a time, so they both use the same hotkey. If both are running, the hotkey will only work for one.

These scripts contain code for collecting the last n keyboard/mouse events. You may reuse this code, but I will not explain how to use it.
User avatar
jNizM
Posts: 2410
Joined: 30 Sep 2013, 01:33
GitHub: jNizM
Contact:

Re: Real-time KeyHistory / MouseHistory

28 Dec 2016, 03:12

Interesting code, thanks for sharing.
[AHK] 1.1.30.01 x64 Unicode | [WIN] 10 Pro (Version 1803) x64 | [GitHub] Profile
Donations are appreciated if I could help you
loek3000
Posts: 143
Joined: 12 Nov 2013, 03:24

Re: Real-time KeyHistory / MouseHistory

30 Dec 2016, 04:10

looks great, mouse history works well, but key not, when i press, the submited key is showed twice...

output is boxed (left bottom) can it be logged into a .txt file too?
Ohnitiel
Posts: 19
Joined: 29 Dec 2016, 09:18

Re: Real-time KeyHistory / MouseHistory

30 Dec 2016, 07:01

loek3000 wrote:looks great, mouse history works well, but key not, when i press, the submited key is showed twice...

output is boxed (left bottom) can it be logged into a .txt file too?
Is there 2 Down and Up events, or is it just one of each? Computers recognize keypress as two separate events, one for when it's pressed (down event), and another when it's released (up event)
loek3000
Posts: 143
Joined: 12 Nov 2013, 03:24

Re: Real-time KeyHistory / MouseHistory

30 Dec 2016, 07:14

aha, yes it's up and down, how to make it just 1 single hit (up / down) and export it to an .txt or .log file?
guest3456
Posts: 2519
Joined: 09 Oct 2013, 10:31

Re: Real-time KeyHistory / MouseHistory

06 Jan 2017, 09:25

GUI Legend comment translated into a header for each:

Code: Select all

Gui, Add, Text,    , VK  SC  Flags     Time  KeyName           Info
Gui, Add, Text, vKH, 00  000  ____  1000.00  Browser_Favorites 0xFFFFFFFF


Resize:
    ; Resize label to fit key history.
    ...
    gui_h += 40

Code: Select all

Gui, Add, Text,    , * Btn         x      y   d/u   time
Gui, Add, Text, vMH, .                                 .


Resize:
    ; Resize label to fit mouse history.
    ...
    gui_h += 50


Return to “Scripts and Functions”

Who is online

Users browsing this forum: DataLife, niczoom and 43 guests