Asynchronous catching of WM_PAINT window message, can it be done ?
Posted: 26 Sep 2023 03:13
Hi,
Today I tried to do probably the impossible.
I feel burned out, let me tell you the story.
I was playing around with powershell hybrid functions
I made one that gets the console's hWND and draws a red circle on it. (using GetConsoleWindow, GetDC, CreateSolidBrush, SelectObject, Ellipse)
And that changes the command window to a "shape". (using GetWindowRect, CreateEllipticRgn, SetWindowRgn)
Draw a red circle
Change window region
With these experiment, I see the entire win32 api is easily accessible to batch via powershell, although it is slow to load the first time.
I quickly noticed that painting on the console hWND is very impermanent.
The drawn pixels will get mangled if the user scroll and will entirely disappears if the user resizes the window.
So I figured, while I am painting, I need to listen for the WM_PAINT messages to know when to redraw the whole window.
This may be impossible.
It all comes down to one of these function calls
WH_GETMESSAGE and WH_CALLWNDPROCRET only seem to work in global more, you need to leave those two last parameter to null/0 or else they will give you error 1429 (must be global).
Likewise WH_CALLWNDPROC will give you error 1428 (can' t be global).
There was another complication with that $threadId !
I don't know how to automatically obtain the correct threadID.
I could only obtain it manually with the software https://github.com/learn-more/WindowsHookEx
This application gave me the thread ID and I could see the WM_PAINT messages I wanted to catch.
From that threadID, I back tracked to the process ID with this powershell code
Then using the task manager, I discovered that this was conhost.exe.
Using GetConsoleWindow, I found my hWND, from my hWND I found my process ID and thread ID, they are not the same as conhost.exe
Worse, there were multiple conhost.exe running on my system. I have failed to discover, which conhost.exe is associated with the PID/TID obtained from GetConsoleWindow
In the end, I did have the correct ThreadID obtained from WindowsHookEx
Would always return error 5 (0x5)/Access is denied./ERROR_INVALID_HANDLE
Even though I was sure of all the values and I tried everything running administrator or user, same thing
For the other two
[/code]
Nothing would happen, until I moved the mouse. I could see in WindowsHookEx, it would do nothing, until there was a message, when it would crash the console to desktop.
https://stackoverflow.com/questions/661 ... 32api-call
https://hinchley.net/articles/creating- ... powershell
https://www.betaarchive.com/wiki/index. ... ive/318804
It is said in many places, that this cannot be done (with managed memory languages). That a DLL has to be injected into.... the intercepted software ? user32.dll ? unclear
At this point I sort of lost where I was going with this !
Two massive, possibly impassable problems.
Anyway, I'm going to make many hybrid batch/powershell functions out of all this, to create simple to use batch functions to call win32 api.
Today I tried to do probably the impossible.
I feel burned out, let me tell you the story.
I was playing around with powershell hybrid functions
I made one that gets the console's hWND and draws a red circle on it. (using GetConsoleWindow, GetDC, CreateSolidBrush, SelectObject, Ellipse)
And that changes the command window to a "shape". (using GetWindowRect, CreateEllipticRgn, SetWindowRgn)
Draw a red circle
Code: Select all
for /f "tokens=*" %%a in ('powershell -command "Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class NativeMethods { [DllImport(\"kernel32.dll\")] public static extern IntPtr GetConsoleWindow(); [DllImport(\"user32.dll\", SetLastError = true)] public static extern IntPtr GetDC(IntPtr hWnd); [DllImport(\"user32.dll\")] public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC); [DllImport(\"gdi32.dll\")] public static extern IntPtr CreateSolidBrush(int crColor); [DllImport(\"gdi32.dll\")] public static extern bool Ellipse(IntPtr hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect); [DllImport(\"gdi32.dll\")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr h); [DllImport(\"gdi32.dll\", SetLastError = true)] public static extern bool DeleteObject(IntPtr ho); }'; $hWndConsole = [NativeMethods]::GetConsoleWindow(); $hDC = [NativeMethods]::GetDC($hWndConsole); $redBrush = [NativeMethods]::CreateSolidBrush(0x0000FF); $oldBrush = [NativeMethods]::SelectObject($hDC, $redBrush); [NativeMethods]::Ellipse($hDC, 0, 0, 100, 100); [NativeMethods]::SelectObject($hDC, $oldBrush); [NativeMethods]::DeleteObject($redBrush); [NativeMethods]::ReleaseDC([IntPtr]::Zero, $hDC);"') do ( break )
Code: Select all
for /f "tokens=*" %%i in ('powershell -command "Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left; public int top; public int right; public int bottom; } public class WindowShape { [DllImport(\"user32.dll\")] public static extern bool SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw); [DllImport(\"gdi32.dll\")] public static extern IntPtr CreateEllipticRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect); [DllImport(\"gdi32.dll\")] public static extern bool DeleteObject(IntPtr hObject); [DllImport(\"user32.dll\")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); }'; $rect = New-Object RECT; [WindowShape]::GetWindowRect([IntPtr]%~1, [ref]$rect); $hRgn = [WindowShape]::CreateEllipticRgn($rect.left, $rect.top, $rect.right, $rect.bottom); [WindowShape]::SetWindowRgn([IntPtr]%~1, $hRgn, $true); [WindowShape]::DeleteObject($hRgn);"') do ( break )
I quickly noticed that painting on the console hWND is very impermanent.
The drawn pixels will get mangled if the user scroll and will entirely disappears if the user resizes the window.
So I figured, while I am painting, I need to listen for the WM_PAINT messages to know when to redraw the whole window.
This may be impossible.
It all comes down to one of these function calls
Code: Select all
$hookId = [NativeMethods]::SetWindowsHookEx([NativeMethods]::WH_CALLWNDPROC, $global:windowProc, $hModule, $threadId)
$hookId = [NativeMethods]::SetWindowsHookEx([NativeMethods]::WH_GETMESSAGE, $global:windowProc, [IntPtr]::Zero, 0)
$hookId = [NativeMethods]::SetWindowsHookEx([NativeMethods]::WH_CALLWNDPROCRET, $global:windowProc, [IntPtr]::Zero, 0)
Likewise WH_CALLWNDPROC will give you error 1428 (can' t be global).
There was another complication with that $threadId !
I don't know how to automatically obtain the correct threadID.
I could only obtain it manually with the software https://github.com/learn-more/WindowsHookEx
This application gave me the thread ID and I could see the WM_PAINT messages I wanted to catch.
From that threadID, I back tracked to the process ID with this powershell code
Code: Select all
$mymanuallyknownthreadId = 8100
$threadHandle = [NativeMethods]::OpenThread([NativeMethods]::THREAD_QUERY_INFORMATION, $false, $mythreadId)
if ($threadHandle -eq [IntPtr]::Zero) {
throw "Failed to open thread."
}
try {
$myprocessId = [NativeMethods]::GetProcessIdOfThread($threadHandle)
} finally {
[void][NativeMethods]::CloseHandle($threadHandle)
}
Using GetConsoleWindow, I found my hWND, from my hWND I found my process ID and thread ID, they are not the same as conhost.exe
Code: Select all
$hWndConsole = [NativeMethods]::GetConsoleWindow()
$processId = 0
$threadId = [NativeMethods]::GetWindowThreadProcessId($hWndConsole, [ref]$processId)
In the end, I did have the correct ThreadID obtained from WindowsHookEx
Code: Select all
$hookId = [NativeMethods]::SetWindowsHookEx([NativeMethods]::WH_CALLWNDPROC, $global:windowProc, $hModule, $threadId)
Even though I was sure of all the values and I tried everything running administrator or user, same thing
For the other two
Code: Select all
$hookId = [NativeMethods]::SetWindowsHookEx([NativeMethods]::WH_GETMESSAGE, $global:windowProc, [IntPtr]::Zero, 0)
$hookId = [NativeMethods]::SetWindowsHookEx([NativeMethods]::WH_CALLWNDPROCRET, $global:windowProc, [IntPtr]::Zero, 0)
Nothing would happen, until I moved the mouse. I could see in WindowsHookEx, it would do nothing, until there was a message, when it would crash the console to desktop.
https://stackoverflow.com/questions/661 ... 32api-call
https://hinchley.net/articles/creating- ... powershell
https://www.betaarchive.com/wiki/index. ... ive/318804
It is said in many places, that this cannot be done (with managed memory languages). That a DLL has to be injected into.... the intercepted software ? user32.dll ? unclear
At this point I sort of lost where I was going with this !
Two massive, possibly impassable problems.
Anyway, I'm going to make many hybrid batch/powershell functions out of all this, to create simple to use batch functions to call win32 api.
Code: Select all
What I most would like to know now is,
How do you find the thread ID that handles the console's messages ?