Button.add_click 中带有变量的启动进程文件路径将无法按预期工作

Start-Process Filepath with Variable in Button.add_click won't work as expected

今天的第二个问题,但这次我相信发生了一些非常奇怪的事情。以下代码不是我的原创,我试图用尽可能少的行来重现该行为。在我的原始代码中,我使用配置文件导入应用程序路径和名称,并在 foreach 循环中调用 CreateButton 函数。

我正在尝试创建多个按钮,其功能应该可以打开不同的应用程序。当我对路径进行硬编码时,一切正常。当我在 foreach 循环中使用变量时,每个按钮都使用提交的最后路径。这是有效的代码:

# Define Form
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object Windows.Forms.Form
$form.Size = New-Object Drawing.Size @(260,250)
$Form.Text = "ToolBox"
$form.StartPosition = "CenterScreen"

# Create Button Function
function CreateButton ($ButtonCommand, $ButtonName, $ButtonLocX, $ButtonLocY, $ButtonSizeX, $ButtonSizeY)
{
$btn = New-Object System.Windows.Forms.Button
$btn.add_click($ButtonCommand)
$btn.Text = $ButtonName
$btn.Location = New-Object System.Drawing.Size($ButtonLocX,$ButtonLocY)
$btn.Size = New-Object System.Drawing.Size($ButtonSizeX,$ButtonSizeY)
$form.Controls.Add($btn)
}

$Command={Start-Process -FilePath "C:\Windows\system32\mmc.exe"}
$Text='MMC'
$LocationX=20
$LocationY=10
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Command={Start-Process -FilePath "C:\Windows\regedit.exe"}
$Text='Regedit'
$LocationX=20
$LocationY=70
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Command={Start-Process -FilePath "C:\Windows\System32\cmd.exe"}
$Text='CMD'
$LocationX=20
$LocationY=100
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

#Show Form
$drc = $form.ShowDialog()

这是无效的代码:

# Define Form
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object Windows.Forms.Form
$form.Size = New-Object Drawing.Size @(260,250)
$Form.Text = "ToolBox"
$form.StartPosition = "CenterScreen"

# Create Button Function
function CreateButton ($ButtonCommand, $ButtonName, $ButtonLocX, $ButtonLocY, $ButtonSizeX, $ButtonSizeY)
{
$btn = New-Object System.Windows.Forms.Button
$btn.add_click($ButtonCommand)
$btn.Text = $ButtonName
$btn.Location = New-Object System.Drawing.Size($ButtonLocX,$ButtonLocY)
$btn.Size = New-Object System.Drawing.Size($ButtonSizeX,$ButtonSizeY)
$form.Controls.Add($btn)
}


$Global:Test="C:\Windows\system32\mmc.exe"
$Command={Start-Process -FilePath $Global:Test}
$Text='MMC'
$LocationX=20
$LocationY=10
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Global:Test="C:\Windows\regedit.exe"
$Command={Start-Process -FilePath $Global:Test}
$Text='Regedit'
$LocationX=20
$LocationY=70
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Global:Test="C:\Windows\System32\cmd.exe"
$Command={Start-Process -FilePath $Global:Test}
$Text='CMD'
$LocationX=20
$LocationY=100
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

#Show Form
$drc = $form.ShowDialog()

第一个示例创建了一个包含 3 个按钮的表单。当我单击每个按钮时,不同的应用程序将启动。第二个例子创建了一个有 3 个按钮的表单。当我点击每个按钮时 "CMD.EXE" 被打开。当我在代码中用 MMC.EXE 块更改 CMD.EXE 时,每个按钮都会打开 MMC。谁能解释一下这是怎么回事?

$Command 块中的 $Global:Test 变量在命令块执行之前不会被解析。也就是说,当它创建命令块时,它确实包含 $Global:Test,而不是您在前一行中设置的值。

因此,当 $Command 块执行时,它会解析变量,该变量是根据脚本中最后的 $Global:Test= 赋值设置的。

基本上你想要的是,同一脚本块的不同实例产生不同的结果。为此,您必须将不同的上下文附加到每个脚本块实例。一种方法是将脚本块绑定到模块。为此,您可以使用 GetNewClosure() 脚本块实例方法:

$ScriptBlockWithContext=$ScriptBlock.GetNewClosure()

此方法将创建新模块并将所有当前作用域变量捕获到其中。如果你想对捕获的内容有更多的控制,你可以自己创建模块:

$Module=New-Module {$Context=$Global:Context}
$ScriptBlockWithContext=$Module.NewBoundScriptBlock($ScriptBlock)

如果你想引用局部变量,那么你可以将它们作为附加参数传递给 New-Module cmdlet:

$Module=New-Module {param($Context)} $Context
$ScriptBlockWithContext=$Module.NewBoundScriptBlock($ScriptBlock)

或者,您可以在创建后调用运算符 &. 来调用模块范围内的任意代码。特别是,您可以使用 New-VariableSet-Variable cmdlet 在模块范围内设置变量:

$Module=New-Module {}
& $Module New-Variable Context $Context
$ScriptBlockWithContext=$Module.NewBoundScriptBlock($ScriptBlock)

此处对您的代码进行简单修改以使其工作:

# Define Form
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object Windows.Forms.Form
$form.Size = New-Object Drawing.Size @(260,250)
$Form.Text = "ToolBox"
$form.StartPosition = "CenterScreen"

# Create Button Function
function CreateButton ($ButtonCommand, $ButtonName, $ButtonLocX, $ButtonLocY, $ButtonSizeX, $ButtonSizeY)
{
$btn = New-Object System.Windows.Forms.Button
$btn.add_click($ButtonCommand)
$btn.Text = $ButtonName
$btn.Location = New-Object System.Drawing.Size($ButtonLocX,$ButtonLocY)
$btn.Size = New-Object System.Drawing.Size($ButtonSizeX,$ButtonSizeY)
$form.Controls.Add($btn)
}

$CaptureCommand={param($Test)}
$CommonCommand={Start-Process -FilePath $Test}

$Module=New-Module $CaptureCommand "C:\Windows\system32\mmc.exe"
$Command=$Module.NewBoundScriptBlock($CommonCommand)
$Text='MMC'
$LocationX=20
$LocationY=10
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Module=New-Module $CaptureCommand "C:\Windows\regedit.exe"
$Command=$Module.NewBoundScriptBlock($CommonCommand)
$Text='Regedit'
$LocationX=20
$LocationY=70
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Module=New-Module $CaptureCommand "C:\Windows\System32\cmd.exe"
$Command=$Module.NewBoundScriptBlock($CommonCommand)
$Text='CMD'
$LocationX=20
$LocationY=100
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

#Show Form
$drc = $form.ShowDialog()