7

I don't remember having any problem finding a window in older Windows OS's, but, I'm not succeeding in Windows 8.1 Update 2 OS, using PowerShell v4.0.

This is the PowerShell v4.0 code I'm using (pretty much trivial):

$sig=@'
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(String sClassName, String sAppName);
'@

$fw = Add-Type -Namespace Win32 -Name Funcs -MemberDefinition $sig -PassThru
$wname='Form1' # any existing window name

$fw::FindWindow($null -as [String], $wname) # returns 0, always!

The last command returns 0, always.

Changing the DllImport attribute to

[DllImport("user32.dll", CharSet = CharSet.Unicode)]

does not change anything; 0 is returned the same way.

Interesting to notice that the equivalent code in C#, returns the correct HWND value.

Does anyone know what's wrong (and how to fix) the PowerShell v4.0 code above?

zyq
  • 153
  • 2
  • 9
  • Did you try FindWindowEx? http://msdn.microsoft.com/en-us/library/windows/desktop/ms633500(v=vs.85).aspx – David Brabant Sep 11 '14 at 06:50
  • This one is for finding child windows. What's wrong with FindWindow()? It's used since long time ago (for top level windows) correctly. Inclusive, it works correctly in C# environment. – zyq Sep 11 '14 at 07:08
  • You can find toplevel windows as well. From the doc: "If hwndParent is NULL, the function uses the desktop window as the parent window. The function searches among windows that are child windows of the desktop." The reason I suggested it is that I found some articles (related to C++) also having trouble with FindWindow under Windows 8.1 and using FindWindowEx as a workaround. – David Brabant Sep 11 '14 at 07:17
  • Can you please post links to those C++ articles? – zyq Sep 11 '14 at 07:22
  • Just to be clear, FindWindowEx() does not work too, unfortunately. If you have those links, I'll appreciate. Thanks. – zyq Sep 11 '14 at 07:34
  • Adding the window classname to the call seems to resolve the issue. Following works for me when trying to find notepad `$fw::FindWindowA('Notepad', $wname) # returns a handle.`. I traced `FindWindow` up into `ZwFindWindowEx` and can see the parameters being passed but it'll require some kernel debugging to know what happens next. My best guess is that the call into the kernel has changed. – Lieven Keersmaekers Sep 11 '14 at 11:00
  • I find a solution working for me changing the Pinvoke template to allow `[IntPtr]::Zero` as first parameter as advised in Microsoft documentation. – JPBlanc Sep 11 '14 at 16:36
  • @zyq do you try it ? – JPBlanc Sep 16 '14 at 20:37
  • Yes, I did. Please, see my reply below your answer. – zyq Sep 24 '14 at 15:36

6 Answers6

10

First : not an answer but to help other people working on it, if you use the good class, for example here I code CalcFrame wich is the real class of the main window of calc.exe it works.

$fw::FindWindow("CalcFrame", $wname) # returns the right value for me if calc.exe is started.

Second : The following works for me ; accordind to Microsoft documentation the first parameter should be null, but accordin to PInvoke site you must pass IntPtr.Zero as the first parameter.

$sig = @"
  [DllImport("user32.dll", CharSet = CharSet.Unicode)]
  public static extern IntPtr FindWindow(IntPtr sClassName, String sAppName);

  [DllImport("kernel32.dll")]
  public static extern uint GetLastError();
"@

$fw = Add-Type -Namespace Win32 -Name Funcs -MemberDefinition $sig -PassThru
$wname='Calculatrice' # any existing window name

$fw::FindWindow([IntPtr]::Zero, $wname ) # returns the Window Handle
$a = $fw::GetLastError()
$a
JPBlanc
  • 70,406
  • 17
  • 130
  • 175
  • Yes, There are 2 WNDCLASS structures being used inside C# implementation, as I explain below. – zyq Sep 24 '14 at 15:37
  • I had a look at the C# implementation for this issue, and found 2 WNDCLASS structures: WNDCLASS_D AND WNDCLASS_I. The former is the traditional structure that uses strings as types, following the Win32 API. But the latter, uses IntPtr.Zero values for those WNDCLASS structure that take null string values. For this reason, specifying null string values will result in noting found because a null string value is not implicitly convertible to IntPtr.Zero. – zyq Sep 24 '14 at 15:44
  • "...Zero is equivalent to null for Windows API functions with parameters or return values that can be either pointers or null..." https://learn.microsoft.com/en-us/dotnet/api/system.intptr.zero?view=net-7.0 – The incredible Jan Dec 08 '22 at 09:35
  • "you must pass IntPtr.Zero as the first parameter" is nowhere to find on pinvoke now but it seems to be true, although it makes no sense. – The incredible Jan Dec 08 '22 at 11:06
2

It seems that the method doesn't fail if, and only if, the ClassName is also specified (cannot be null) like in this example:

$sig=@'
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
'@    

$w32 = Add-Type -Namespace Win32 -Name Funcs -MemberDefinition $sig -PassThru
$w32::FindWindow('ConsoleWindowClass', 'Windows PowerShell') # Windows PowerShell Console

If the ClassName is null, then the JPBlanc's method works correctly, which specifies a different signature for the method.

2

The following code works fine for me, thanks to @zyq's advice

Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.MessageBox]::Show('Test', 'PowerShell Dialog', 
        [Windows.Forms.MessageBoxButtons]::OK, 
        [Windows.Forms.MessageBoxIcon]::Information, 
        [Windows.Forms.MessageBoxDefaultButton]::Button1,
        [Windows.Forms.MessageBoxOptions]::ServiceNotification
        )

$Win32API = Add-Type -Name Funcs -Namespace Win32 -PassThru -MemberDefinition @'
    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr FindWindow(string lpClassName, IntPtr lpWindowName);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr FindWindow(IntPtr lpClassName, string lpWindowName);
'@

$Win32API::FindWindow('#32770',       'PowerShell Dialog')
$Win32API::FindWindow([IntPtr]::Zero, 'PowerShell Dialog')
$Win32API::FindWindow('#32770',       [IntPtr]::Zero)
$Win32API::FindWindow('Notepad',      [IntPtr]::Zero)
Carson
  • 6,105
  • 2
  • 37
  • 45
Valdemar_Rudolfovich
  • 3,021
  • 2
  • 18
  • 17
  • 1
    I have always wondered why passing Null always fails. It turns out that the declaration must include all types. Nice answer! – Carson Jul 06 '23 at 10:20
1

I had a look at the C# implementation for this issue, and found 2 WNDCLASS structures: WNDCLASS_D AND WNDCLASS_I. The former is the traditional structure that uses strings as types, following the Win32 API. But the latter, uses IntPtr.Zero values for those WNDCLASS structure that take null string values. For this reason, specifying null string values will result in noting found because a null string value is not implicitly convertible to IntPtr.Zero.

zyq
  • 153
  • 2
  • 9
  • If you say so, no problem. I marked your reply as correct instead of mine. – zyq Sep 25 '14 at 06:02
  • 1
    It's as you want, but I just don't understand why you do not put your your answer as a comment of my one ? – JPBlanc Sep 25 '14 at 06:07
  • It is there much before your reply above. Your answer is not an answer in your own words. Also, I posted my reply as additional answer, because it tells why the code must use IntPtr.Zero and not just that it must use it. But I do appreciate your reply. – zyq Sep 27 '14 at 01:47
  • Many thanks, replacing $null with [IntPtr]::Zero works for me – Valdemar_Rudolfovich Feb 09 '18 at 04:09
0

The problem is trying to pass $null to a string-typed parameter, which results in an empty string instead. This means that FindWindow will search for an empty class name, which will never succeed.

This used to be a well-known limitation and has been the reason NullString was introduced (see this answer for more information).

Borrowing some code from Valdemar's answer, the following should work:

$Win32API = Add-Type -Name Funcs -Namespace Win32 -PassThru -MemberDefinition @'
    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
'@

# find window by title only
$Win32API::FindWindow([NullString]::Value, 'window title')
efotinis
  • 14,565
  • 6
  • 31
  • 36
0

I have packaged it into a function for the convenience of use.

function Find-Window {
    <#
    .Synopsis
        user32dll.FindWindow(className, windowName)
    .Outputs
        @{
           Hwnd
           Err
        }
    .Example
        Find-Window 'ConsoleWindowClass' 'Windows PowerShell'
        Find-Window ApplicationFrameWindow 小算盤
        fWin 'ConsoleWindowClass' 'Windows PowerShell'
    .Example
        # class only
        Find-Window Notepad
    .Example
        # windowName only
        Find-Window -windowName Calculatrice
        Find-Window -windowName 小算盤
    #>
    [alias('fWin')]
    param (
        [Parameter()]
        [string]$className = "",
        [Parameter()]
        [string]$windowName = ""
    )

    $sig=@'
    // https://stackoverflow.com/a/48698671/9935654
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr FindWindow(string lpClassName, IntPtr lpWindowName);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr FindWindow(IntPtr lpClassName, string lpWindowName);

    [DllImport("kernel32.dll")]
    public static extern uint GetLastError();
'@

    $w32 = Add-Type -Namespace Win32 -Name Funcs -MemberDefinition $sig -PassThru
    $cName = if ($className -eq "") {[IntPtr]::Zero} else {$className}
    $wName = if ($windowName -eq "") {[IntPtr]::Zero} else {$windowName}
    $r = $w32::FindWindow($cName, $wName)
    $o = @{
       Hwnd = 0
       Err = $null
    }
    if ($r -eq 0) {
        $o.Err = $w32::GetLastError()
        return $o
    }
    $o.Hwnd = $r
    return $o
}
Carson
  • 6,105
  • 2
  • 37
  • 45