65.9K
CodeProject 正在变化。 阅读更多。
Home

通过基本 Windows 窗体处理 PowerShell 输入

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (38投票s)

2014年7月21日

CPOL

91分钟阅读

viewsIcon

226813

downloadIcon

2217

本文档给出了一些在 PowerShell 脚本中嵌入基本 Windows 窗体和 WPF 以收集用户输入的示例。

目录

引言

PowerShell 是一个高级脚本框架,通常脚本在控制台主机中运行,最常见的是远程运行,但 PowerShell 脚本在 Windows 计算机上仍然相对频繁地用于交互式操作。当一个通用脚本执行时,它很可能需要选择一个以上的选项。需要以级联的方式向用户提供多个选项,并且通常需要复杂的选择场景。对于某些数据选择,GUI 比 CLI 更直观、更快——在控制台中,即使是基本选择看起来也不太好。

在许多情况下,老式的 Windows Forms 仍然是提示用户的一种便捷方式。这是本文的主要重点。我们检查了来自 http://www.java2s.com/ 的几个基本示例,并将它们转换为 PowerShell。稍后,我们将使用早期的示例作为构建更复杂事物的基石。所有这些示例的代码都包含在一个文件中,并且不需要合并单独的设计师代码,这大大简化了转换。重点在于保持新兴的 PowerShell 代码尽可能少,以处理各种数据选择场景,包括提示、密码、复选框、单选按钮、选中列表、网格、树视图、制表对话框以及它们的组合。此外,还将演示窗体元素特定的事件处理程序将执行 PowerShell 代码。最后,像 TreeView 这样的控件本身就能很好地可视化数据,并可能使几次提示变得不必要。

另一方面,Windows Presentation Foundation 可能会感觉有点沉重难以启动和/或调试,但完全可行——本文档中间提供了示例。与 WPF 交互需要多线程,而此技术对于长运行脚本的异步状态报告也很有价值。

一个令人欣慰的注意点是,所有脚本在最小服务器界面(Minimal Server Interface)甚至服务器核心(Server Core)Windows Server 2012 GUI levels 下仍然有效。原因如下:即使在“服务器图形 shell”(Server Graphical Shell)和“服务器图形管理工具和基础架构”(Server Graphical Management Tools & Infrastructure)Windows 功能被“移除”之后,完整的 Microsoft .Net Framework 仍然存在。示例的最终目标是为复杂自定义数据提供熟悉的 UI——这仍然可以在 Windows Server Core 上实现。请注意,由于即使在 Server Core 中也可用鼠标,因此不需要为窗体元素添加键盘快捷键。

在后续示例中,将展示如何手动将 PowerShell Selenium 脚本从 C# 等效脚本构建,或自动在 Selenium IDE 中录制;并说明使用 PowerShell 运行 Selenium 录制的明显好处。

最后,详细介绍了分步转换过程。

背景

人们会发现 PowerShell 版本代码与 C# 版本几乎相同,只有语义差异。所有源代码均可在作者的 github 存储库中找到,并且每天都在开发新代码。

目前,我们需要构建一个辅助类,负责将信息传递给 PowerShell 脚本调用者,使用纯 C# 编写,并使其属性在事件处理程序中对 Windows 窗体可用,尽管所有对话框都将以模态方式绘制。如果没有这种紧密的联系,可能会出现一些难以调试的竞争条件错误。这些假设的分析被推迟到未来的文章。

Using the Code

本文档提供的示例希望读者能够轻松地根据自己的需求进行定制。

代码详情

将用于在窗体和 PowerShell 之间共享信息的类非常基础。它只需要实现 IWin32Window 接口;它还将具有各种 private 数据成员,以及用于在下面一些示例的窗体中使用的 getter、setter 和方法。

Add-Type -TypeDefinition @"

// "
using System;
using System.Windows.Forms;
public class Win32Window : IWin32Window
{
    private IntPtr _hWnd;
    private int _data;

    public int Data
    {
        get { return _data; }
        set { _data = value; }
    }

    public Win32Window(IntPtr handle)
    {
        _hWnd = handle;
    }

    public IntPtr Handle
    {
        get { return _hWnd; }
    }
}

"@ -ReferencedAssemblies 'System.Windows.Forms.dll'

PowerShell 将其自己的窗口句柄存储在类中

if ($process_window -eq $null ){
  $process_window = New-Object Win32Window -ArgumentList
  ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
}

条目选择和整体状态从 $caller.Message$caller.Data 读取

$DebugPreference = 'Continue'

  if($process_window.Data -ne $RESULT_CANCEL) {
    write-debug ('Selection is : {0}' -f  , $process_window.Message )
  } else {
    write-debug ('Result is : {0} ({1})' -f
    $Readable.Item($process_window.Data) , $process_window.Data )
  }

备用语法可以是

$guid = [guid]::NewGuid()

$helper_namespace = ("Util_{0}" -f ($guid -replace '-',''))
$helper_name = 'Helper'

Add-Type -UsingNamespace @(
  'System.Drawing',
  'System.IO',
  'System.Windows.Forms',
  'System.Drawing.Imaging',
  'System.Collections.Generic',
  'System.Text' `
  ) `
   -MemberDefinition @"
// inline C# code without class decoration
"@ -ReferencedAssemblies @( 'System.Windows.Forms.dll',`
     'System.Drawing.dll',`
     'System.Data.dll',`
     'System.Xml.dll') `
   -Namespace $helper_namespace -Name $helper_name -ErrorAction Stop

$helper = New-Object -TypeName ('{0}.{1}' -f $helper_namespace,$helper_type)
# the rest of Powershell code 

这样,每次修改内联 C# 代码时,您就不必担心看到烦人的警告了

Add-Type : Cannot add type. The type name 'Win32Window' already exists.
At C:\developer\sergueik\powershell_ui_samples\treeview_c.ps1:21 char:1
+ Add-Type -TypeDefinition @"

请注意,一些命名空间已经默认包含,不应显式提供在调用参数中,以避免

 Warning as Error: 
 The using directive for 'System' appeared previously in this namespace
 The using directive for 'System.Runtime.InteropServices' appeared previously in this namespace

多项选择提示

Three button prompt

多项选择决策提示是最简单的示例,它不需要窗体元素之间进行任何通信——窗体在每个按钮的 Click 事件处理程序中独立设置 $caller.Data

function PromptAuto(
    [String] $title,
    [String] $message,
    [Object] $caller = $null
    ){

[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')

$f = New-Object System.Windows.Forms.Form
$f.Text = $title

$f.Size = New-Object System.Drawing.Size(650,120)
$f.StartPosition = 'CenterScreen'

$f.KeyPreview = $True
$f.Add_KeyDown({

    if     ($_.KeyCode -eq 'Y')       { $caller.Data = $RESULT_POSITIVE }
    elseif ($_.KeyCode -eq 'N')       { $caller.Data = $RESULT_NEGATIVE }
    elseif ($_.KeyCode -eq 'Escape')  { $caller.Data = $RESULT_CANCEL }
    else                              { return }
    $f.Close()

})

$b1 = New-Object System.Windows.Forms.Button
$b1.Location = New-Object System.Drawing.Size(50,40)
$b1.Size = New-Object System.Drawing.Size(75,23)
$b1.Text = 'Yes!'
$b1.Add_Click({ $caller.Data = $RESULT_POSITIVE; $f.Close(); })

$b2 = New-Object System.Windows.Forms.Button
$b2.Location = New-Object System.Drawing.Size(125,40)
$b2.Size = New-Object System.Drawing.Size(75,23)
$b2.Text = 'No!'
$b2.Add_Click({ $caller.Data = $RESULT_NEGATIVE; $f.Close(); })

$b3 = New-Object System.Windows.Forms.Button
$b3.Location = New-Object System.Drawing.Size(200,40)
$b3.Size = New-Object System.Drawing.Size(75,23)
$b3.Text = 'Maybe'
$b3.Add_Click({$caller.Data =  $RESULT_CANCEL ; $f.Close()})

$l = New-Object System.Windows.Forms.Label
$l.Location = New-Object System.Drawing.Size(10,20)
$l.Size = New-Object System.Drawing.Size(280,20)
$l.Text = $message

$f.Controls.Add($b1)
$f.Controls.Add($b3)
$f.Controls.Add($b2)
$f.Controls.Add($l)
$f.Topmost = $True

if ($caller -eq $null ){
  $caller = New-Object Win32Window -ArgumentList
  ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
}

$caller.Data = $RESULT_CANCEL;
$f.Add_Shown( { $f.Activate() } )

[void] $f.ShowDialog([Win32Window ] ($caller) )
$f.Dispose()
}

选项文本和定义在函数中硬编码。

$RESULT_POSITIVE = 0
$RESULT_NEGATIVE = 1
$RESULT_CANCEL = 2

$Readable = @{
    $RESULT_NEGATIVE = 'NO!';
    $RESULT_POSITIVE = 'YES!' ;
    $RESULT_CANCEL = 'MAYBE...'
    }

$process_window = New-Object Win32Window -ArgumentList
([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

$title = 'Question'
$message =  "Continue to Next step?"

$result = PromptAuto -title  $title -message $message  -caller $process_window

write-debug ("Result is : {0} ({1})" -f $Readable.Item($process_window.Data) , $process_window.Data )

超时提示

关闭空闲输入框的流行功能可以通过例如添加一个包含 System.Timers.TimerSystem.Windows.Forms.Panel 子类来提供

using System;
using System.Drawing;
using System.Windows.Forms;

public class TimerPanel : System.Windows.Forms.Panel
{
    private System.Timers.Timer _timer;
    private System.ComponentModel.Container components = null;
    public System.Timers.Timer Timer
    {
        get
        {
            return _timer;
        }
        set { _timer = value; }
    }

    public TimerPanel()
    {
        InitializeComponent();
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (components != null)
            {
                components.Dispose();
            }
        }
        _timer.Stop();
        base.Dispose(disposing);
    }

    private void InitializeComponent()
    {
        this._timer = new System.Timers.Timer();
        ((System.ComponentModel.ISupportInitialize)(this._timer)).BeginInit();
        this.SuspendLayout();
        this._timer.Interval = 1000;
        this._timer.Start();
        this._timer.Enabled = true;
        this._timer.SynchronizingObject = this;
        this._timer.Elapsed += new System.Timers.ElapsedEventHandler(this.OnTimerElapsed);
        ((System.ComponentModel.ISupportInitialize)(this._timer)).EndInit();
        this.ResumeLayout(false);
    }

    private void OnTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        // Console.WriteLine(".");
    }

}

然后将所有输入放在面板上。

  $p = New-Object TimerPanel
  $p.Size = $f.Size
  $end = (Get-Date -UFormat "%s")
  $end = ([int]$end + 60)
  $p.Timer.Stop()
  $p.Timer.Interval = 5000;
  $p.Timer.Start()
  $p.Timer.add_Elapsed({
      $start = (Get-Date -UFormat "%s")

      $elapsed = New-TimeSpan -Seconds ($start - $end)
      $l.Text = ('Remaining time {0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds,($end - $start))

      if ($end - $start -lt 0) {
        $caller.Data = $RESULT_TIMEOUT;
        $f.Close()
      }

    })

Timer 的属性和方法是公共的,因此脚本提供了事件处理程序——在上面的示例中,一分钟的间隔(以秒为单位)被硬编码了。

timing out prompt

完整的示例显示在下面,并在源 zip 文件中提供。

$RESULT_OK = 0
$RESULT_CANCEL = 1
$RESULT_TIMEOUT = 2

$Readable = @{
  $RESULT_OK = 'OK';
  $RESULT_CANCEL = 'CANCEL';
  $RESULT_TIMEOUT = 'TIMEOUT';
}

function PromptTimedAutoClose  {

param(
  [string]$title,
  [string]$message,
  [object]$caller
)

  [void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
  [void][System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')

  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title

  $f.Size = New-Object System.Drawing.Size (240,110)

  $f.StartPosition = 'CenterScreen'

  $f.KeyPreview = $True
  $f.Add_KeyDown({

      if ($_.KeyCode -eq 'O') { $caller.Data = $RESULT_OK }
      elseif ($_.KeyCode -eq 'Escape') { $caller.Data = $RESULT_CANCEL }
      else { return }
      $f.Close()

    })

  $b1 = New-Object System.Windows.Forms.Button
  $b1.Location = New-Object System.Drawing.Size (50,40)
  $b1.Size = New-Object System.Drawing.Size (75,23)
  $b1.Text = 'OK'
  $b1.add_click({ $caller.Data = $RESULT_OK; $f.Close(); })
  $p = New-Object TimerPanel
  $p.Size = $f.Size

  $p.Controls.Add($b1)
  $end = (Get-Date -UFormat "%s")
  $end = ([int]$end + 60)
  $b2 = New-Object System.Windows.Forms.Button
  $b2.Location = New-Object System.Drawing.Size (130,40)
  $b2.Size = New-Object System.Drawing.Size (75,23)
  $b2.Text = 'Cancel'
  $b2.add_click({
      $caller.Data = $RESULT_CANCEL;
      $f.Close();
    })
  $p.Controls.Add($b2)

  $l = New-Object System.Windows.Forms.Label
  $l.Location = New-Object System.Drawing.Size (10,20)
  $l.Size = New-Object System.Drawing.Size (280,20)
  $l.Text = $message
  $p.Controls.Add($l)

  $p.Timer.Stop()
  $p.Timer.Interval = 5000;
  $p.Timer.Start()
  $p.Timer.add_Elapsed({
      $start = (Get-Date -UFormat "%s")

      $elapsed = New-TimeSpan -Seconds ($start - $end)
      $l.Text = ('Remaining time {0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds,($end - $start))

      if ($end - $start -lt 0) {
        $caller.Data = $RESULT_TIMEOUT;
        $f.Close()
      }

    })
  $f.Controls.Add($p)
  $f.Topmost = $True

  $caller.Data = $RESULT_TIMEOUT;
  $f.Add_Shown({ $f.Activate() })

  [void]$f.ShowDialog([win32window ]($caller))
  $f.Dispose()
}

$DebugPreference = 'Continue'
$title = 'Prompt w/timeout'
$message = "Continue ?"
$caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

PromptTimedAutoClose -Title $title -Message $message -caller $caller
$result = $caller.Data
Write-Debug ("Result is : {0} ({1})" -f $Readable.Item($result),$result)

收集复选框和单选按钮组的选择

button broups

此示例代码更有趣,因为脚本将收集多个分组元素的的状态。管理单个 checkbox radiobutton 的行为保持不变,只实现按钮 Click 处理程序,其中窗体绘制选定元素的摘要并将其存储在 $caller 中——为简单起见,$shapes$color 都放入一个 $caller.Message 中。

function PromptWithCheckboxesAndRadionbuttons(
    [String] $title,
    [String] $message,
    [Object] $caller = $null
    ){

  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Data')

  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title
  $groupBox1 = New-Object System.Windows.Forms.GroupBox
  $checkBox1 = New-Object System.Windows.Forms.CheckBox
  $checkBox2 = New-Object System.Windows.Forms.CheckBox
  $checkBox3 = New-Object System.Windows.Forms.CheckBox
  $radioButton1 = New-Object System.Windows.Forms.RadioButton
  $radioButton2 = New-Object System.Windows.Forms.RadioButton
  $radioButton3 = New-Object System.Windows.Forms.RadioButton
  $button1  = New-Object System.Windows.Forms.Button
  $components =  New-Object System.ComponentModel.Container

  $groupBox1.SuspendLayout()
  $f.SuspendLayout()
  $color = ''
  $shapes = @()

  # groupBox1
  $groupBox1.Controls.AddRange(
     @(
       $radioButton1,
       $radioButton2,
       $radioButton3
      ))
  $groupBox1.Location = New-Object System.Drawing.Point(8, 120)
  $groupBox1.Name = 'groupBox1'
  $groupBox1.Size = New-Object System.Drawing.Size(120, 144)
  $groupBox1.TabIndex = 0
  $groupBox1.TabStop = $false
  $groupBox1.Text = 'Color'

  # checkBox1
  $checkBox1.Location = New-Object System.Drawing.Point(8, 8)
  $checkBox1.Name = 'checkBox1'
  $checkBox1.TabIndex = 1
  $checkBox1.Text = 'Circle'

  # checkBox2

  $checkBox2.Location = New-Object System.Drawing.Point(8, 40)
  $checkBox2.Name = 'checkBox2'
  $checkBox2.TabIndex = 2
  $checkBox2.Text = 'Rectangle'

  # checkBox3

  $checkBox3.Location = New-Object System.Drawing.Point(8, 72)
  $checkBox3.Name = 'checkBox3'
  $checkBox3.TabIndex = 3
  $checkBox3.Text = 'Triangle'

  # radioButton1

  $radioButton1.Location = New-Object System.Drawing.Point(8, 32)
  $radioButton1.Name = 'radioButton1'
  $radioButton1.TabIndex = 4
  $radioButton1.Text = 'Red'
  $radioButton1.Add_CheckedChanged({ })

  # radioButton2

  $radioButton2.Location = New-Object System.Drawing.Point(8, 64)
  $radioButton2.Name = 'radioButton2'
  $radioButton2.TabIndex = 5
  $radioButton2.Text = 'Green'

  # radioButton3

  $radioButton3.Location = New-Object System.Drawing.Point(8, 96)
  $radioButton3.Name = 'radioButton3'
  $radioButton3.TabIndex = 6
  $radioButton3.Text = 'Blue'

  # button1

  $button1.Location = New-Object System.Drawing.Point(8, 280)
  $button1.Name = 'button1'
  $button1.Size = New-Object System.Drawing.Size(112, 32)
  $button1.TabIndex = 4
  $button1.Text = 'Draw'

  $button1.Add_Click({

  $color = ''
  $shapes = @()
  foreach ($o in @($radioButton1, $radioButton2, $radioButton3)){
  if ($o.Checked){
      $color = $o.Text}
  }
  foreach ($o in @($checkBox1, $checkBox2, $checkBox3)){
  if ($o.Checked){
      $shapes += $o.Text}

  }
  $g = [System.Drawing.Graphics]::FromHwnd($f.Handle)
  $rc = New-Object System.Drawing.Rectangle(150, 50, 250, 250)
  $brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
  $g.FillRectangle($brush, $rc)
  $font = New-Object System.Drawing.Font('Verdana', 12)
  $col = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::Black)
  $str = [String]::Join(';', $shapes )
  $pos1 = New-Object System.Drawing.PointF(160, 60)
  $pos2 = New-Object System.Drawing.PointF(160, 80)

  $g.DrawString($color, $font, $col , $pos1)
  $g.DrawString($str, $font, $col , $pos2)
  start-sleep 1

  $caller.Message =  ('color:{0} shapes:{1}' -f $color , $str)

  $f.Close()
 })

  # Form1

  $f.AutoScaleBaseSize = New-Object System.Drawing.Size(5, 13)
  $f.ClientSize = New-Object System.Drawing.Size(408, 317)
  $f.Controls.AddRange( @(
     $button1,
     $checkBox3,
     $checkBox2,
     $checkBox1,
     $groupBox1))

  $f.Name = 'Form1'
  $f.Text = 'CheckBox and RadioButton Sample'
  $groupBox1.ResumeLayout($false)
  $f.ResumeLayout($false)

  $f.StartPosition = 'CenterScreen'

  $f.KeyPreview = $True

  $f.Add_KeyDown({

    if ($_.KeyCode -eq 'Escape')  { $caller.Data = $RESULT_CANCEL }
    else          {  }
    $f.Close()
  })

  $f.Topmost = $True
  if ($caller -eq $null ){
    $caller = New-Object Win32Window -ArgumentList
    ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
  }

  $f.Add_Shown( { $f.Activate() } )

  [Void] $f.ShowDialog([Win32Window ] ($caller) )
  $F.Dispose()

  return $caller.Data
}

列表框选择

listboxes

下一个迭代是让窗体从 PowerShell 接收文本字符串,并将单个单词显示为选中的 listbox 项,等待用户通过单击单词旁边的 checkbox 来选择各个单词。

$DebugPreference = 'Continue'
$result = PromptCheckedList ''  'Lorem ipsum dolor sit amet, consectetur adipisicing elit'

write-debug ('Selection is : {0}' -f  , $result )

右侧的 listbox 为用户提供视觉提示。按下“完成”按钮后,选择将保存在 $caller 对象中,窗体关闭并被处置。

这次,我们显式返回 $caller.Message,尽管它不是真正必需的。请注意粗体显示的事件处理程序代码。

function PromptCheckedList
{
     Param(
    [String] $title,
    [String] $message)

  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections.Generic')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Text')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Data')

  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title

  $i = new-object System.Windows.Forms.CheckedListBox
  $d = new-object System.Windows.Forms.ListBox
  $d.SuspendLayout()
  $i.SuspendLayout()
  $f.SuspendLayout()
  $i.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 11,
  [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);
  $i.FormattingEnabled = $true;
  $i.Items.AddRange(( $message -split '[ ,]+' ));

  $i.Location = New-Object System.Drawing.Point(17, 12)
  $i.Name = 'inputCheckedListBox'
  $i.Size = New-Object System.Drawing.Size(202, 188)
  $i.TabIndex = 0
  $i.TabStop = $false

  $event_handler = {
       param(
            [Object] $sender,
            [System.Windows.Forms.ItemCheckEventArgs ] $eventargs
         )
         $item = $i.SelectedItem
         if ( $eventargs.NewValue -eq  [System.Windows.Forms.CheckState]::Checked ) {
            $d.Items.Add( $item );
         } else {
            $d.Items.Remove( $item );
         }
  }
  $i.Add_ItemCheck($event_handler)

  $d.Font = New-Object System.Drawing.Font('Verdana', 11)
  $d.FormattingEnabled = $true
  $d.ItemHeight = 20;
  $d.Location =  New-Object System.Drawing.Point(236, 12);
  $d.Name = 'displayListBox';
  $d.Size = New-Object System.Drawing.Size(190, 184);
  $d.TabIndex = 1;

  $b  = New-Object System.Windows.Forms.Button
  $b.Location = New-Object System.Drawing.Point(8, 280)
  $b.Name = 'button1'
  $b.Size = New-Object System.Drawing.Size(112, 32)
  $b.TabIndex = 4
  $b.Text = 'Done'

  $b.Add_Click({
    $shapes = @()
    foreach ($o in $d.Items){
      $shapes += $o
    }
    $caller.Message =  [String]::Join(';', $shapes )
    $f.Close()
 })

  $f.AutoScaleBaseSize = New-Object System.Drawing.Size(5, 13)
  $f.ClientSize = New-Object System.Drawing.Size(408, 317)
  $components =  New-Object System.ComponentModel.Container

  $f.Controls.AddRange( @( $i, $d, $b))

  $f.Name = 'Form1'
  $f.Text = 'CheckListBox Sample'
  $i.ResumeLayout($false)
  $d.ResumeLayout($false)

  $f.ResumeLayout($false)

  $f.StartPosition = 'CenterScreen'

  $f.KeyPreview = $True

  $f.Topmost = $True
  $caller = New-Object Win32Window -ArgumentList
  ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

  $f.Add_Shown( { $f.Activate() } )

  [Void] $f.ShowDialog([Win32Window ] ($caller) )
  $f.Dispose()
  $result = $caller.Message
  $caller = $null
  return $result
}

这里,事件处理程序是用 PowerShell 编写的,但它操作标准的事件参数,因此 PowerShell 函数是从 Form 元素调用的,基本上将它们相互连接起来。它几乎与已转换的类方法无法区分来自

this.inputCheckedListBox.ItemCheck +=
new System.Windows.Forms.ItemCheckEventHandler(this.inputCheckedListBox_ItemCheck);

...

private void inputCheckedListBox_ItemCheck(object sender, ItemCheckEventArgs e )
      {
         string item = inputCheckedListBox.SelectedItem.ToString();

         if ( e.NewValue == CheckState.Checked )
            displayListBox.Items.Add( item );
         else
            displayListBox.Items.Remove( item );
      }

手风琴菜单

下一个示例来自将 手风琴可折叠面板 从 C# 转换为 PowerShell。当然,代码非常冗余。只显示一部分。完整脚本在源 zip 中。

collapsed

$caller = New-Object -TypeName 'Win32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

@( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }

$f = New-Object -TypeName 'System.Windows.Forms.Form'
$f.Text = $title
$f.SuspendLayout()

$p = New-Object System.Windows.Forms.Panel
$m = New-Object System.Windows.Forms.Panel
$p_3 = New-Object System.Windows.Forms.Panel
$b_3_3 = New-Object System.Windows.Forms.Button
$b_3_2 = New-Object System.Windows.Forms.Button
$b_3_1 = New-Object System.Windows.Forms.Button
$g_3 = New-Object System.Windows.Forms.Button
$p_2 = New-Object System.Windows.Forms.Panel

$b_2_4 = New-Object System.Windows.Forms.Button
$b_2_3 = New-Object System.Windows.Forms.Button
$b_2_2 = New-Object System.Windows.Forms.Button
$b_2_1 = New-Object System.Windows.Forms.Button
$g_2 = New-Object System.Windows.Forms.Button
$p_1 = New-Object System.Windows.Forms.Panel
$b_1_2 = New-Object System.Windows.Forms.Button
$b_1_1 = New-Object System.Windows.Forms.Button
$g_1 = New-Object System.Windows.Forms.Button
$lblMenu = New-Object System.Windows.Forms.Label
$m.SuspendLayout()
$p_3.SuspendLayout()
$p_2.SuspendLayout()
$p_1.SuspendLayout()
$p.SuspendLayout()

..
#  Panel Menu 1
$p_1.Controls.AddRange(@($b_1_2, $b_1_1,  $g_1) )
$p_1.Dock = [System.Windows.Forms.DockStyle]::Top
$p_1.Location = New-Object System.Drawing.Point (0,23)
$p_1.Name = "p_1"
# $p_1.Size = New-Object System.Drawing.Size ($global:button_panel_width,104)
$p_1.TabIndex = 1

#  Menu 1 button 1
$b_1_1.BackColor = [System.Drawing.Color]::Silver
$b_1_1.Dock = [System.Windows.Forms.DockStyle]::Top
$b_1_1.FlatAppearance.BorderColor = [System.Drawing.Color]::DarkGray
$b_1_1.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$b_1_1.Location = New-Object System.Drawing.Point (0,($global:button_panel_height * 2))

$b_1_1.Name = "b_1_1"
$b_1_1.Size = New-Object System.Drawing.Size ($global:button_panel_width,$global:button_panel_height)
$b_1_1.TabIndex = 2
$b_1_1.Text = "Group 1 Sub Menu 1"
$b_1_1.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
$b_1_1.UseVisualStyleBackColor = $false
$b_1_1_click = $b_1_1.add_Click
$b_1_1_click.Invoke({

    param([object]$sender,[string]$message)
    $caller.Data = $sender.Text
    [System.Windows.Forms.MessageBox]::Show(('{0} clicked!' -f $sender.Text) )
  })

#  Menu 1 button 2
$b_1_2.BackColor = [System.Drawing.Color]::Silver
$b_1_2.Dock = [System.Windows.Forms.DockStyle]::Top
$b_1_2.FlatAppearance.BorderColor = [System.Drawing.Color]::DarkGray
$b_1_2.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$b_1_2.Location = New-Object System.Drawing.Point (0,($global:button_panel_height * 3))

$b_1_2.Name = "$b_1_2"
$b_1_2.Size = New-Object System.Drawing.Size ($global:button_panel_width,$global:button_panel_height)
$b_1_2.TabIndex = 3
$b_1_2.Text = "Group 1 Sub Menu 2"
$b_1_2.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
$b_1_2.UseVisualStyleBackColor = $false

#  Menu 1 button group 
$g_1.BackColor = [System.Drawing.Color]::Gray
$g_1.Dock = [System.Windows.Forms.DockStyle]::Top
$g_1.FlatAppearance.BorderColor = [System.Drawing.Color]::Gray
$g_1.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$g_1.ImageAlign = [System.Drawing.ContentAlignment]::MiddleRight
$g_1.Location = New-Object System.Drawing.Point (0,0)
$g_1.Name = "g_1"
$g_1.Size = New-Object System.Drawing.Size ($global:button_panel_width,$global:button_panel_height)
$g_1.TabIndex = 0
$g_1.Text = "Menu Group 1"
$g_1.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
$g_1.UseVisualStyleBackColor = $false
$g_1_click = $g_1.add_click
$g_1_click.Invoke({
    param(
      [object]$sender,
      [System.EventArgs]$eventargs
    )

    $ref_panel = ([ref]$p_1)
    $ref_button_menu_group = ([ref]$g_1)
    $num_buttons = 3
    # use the current height of the element as indicator of its state.
    if ($ref_panel.Value.Height -eq $global:button_panel_height)
    {
      $ref_panel.Value.Height = ($global:button_panel_height * $num_buttons) + 2
      $ref_button_menu_group.Value.Image = New-Object System.Drawing.Bitmap ("C:\developer\sergueik\powershell_ui_samples\unfinished\up.png")
    }
    else
    {
      $ref_panel.Value.Height = $global:button_panel_height
      $ref_button_menu_group.Value.Image = New-Object System.Drawing.Bitmap ("C:\developer\sergueik\powershell_ui_samples\unfinished\down.png")
    }
  })


$m.ResumeLayout($false)
$p_3.ResumeLayout($false)
$p_2.ResumeLayout($false)
$p_1.ResumeLayout($false)
$p.ResumeLayout($false)


$f.Controls.Add($p)
#  Form1
$f.AutoScaleDimensions = New-Object System.Drawing.SizeF (6.0,13.0)
$f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
$f.ClientSize = New-Object System.Drawing.Size (210,280)
$f.Controls.Add($c1)
$f.Controls.Add($p)
$f.Controls.Add($b1)
$f.Name = "Form1"
$f.Text = "ProgressCircle"
$f.ResumeLayout($false)

$f.Topmost = $True

$f.Add_Shown({ $f.Activate() })

[void]$f.ShowDialog([win32window]($caller))

$f.Dispose()

expanded

为了对抗冗余,可以引入实用程序函数,例如

function add_button {
  param(
    [System.Management.Automation.PSReference]$button_data_ref,
    [System.Management.Automation.PSReference]$button_ref
  )

  $button_data = $button_data_ref.Value

  #  TODO: assert ?

  $local:b = $button_ref.Value
  $local:b.BackColor = [System.Drawing.Color]::Silver
  $local:b.Dock = [System.Windows.Forms.DockStyle]::Top
  $local:b.FlatAppearance.BorderColor = [System.Drawing.Color]::DarkGray
  $local:b.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
  $local:b.Location = New-Object System.Drawing.Point (0,($global:button_panel_height * $button_data['cnt']))
  $local:b.Size = New-Object System.Drawing.Size ($global:button_panel_width,$global:button_panel_height)
  $local:b.TabIndex = 3
  $local:b.Name = $button_data['name']
  $local:b.Text = $button_data['text']
  $local:b.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
  $local:b.UseVisualStyleBackColor = $false

  $local:click_handler = $local:b.add_Click
  if ($button_data.ContainsKey('callback')) {
    $local:click_handler.Invoke($button_data['callback'])
  }

  else {
    # provide default click handler

    $local:click_handler.Invoke({

        param(
          [object]$sender,
          [System.EventArgs]$eventargs
        )
        $caller.Data = $sender.Text
        [System.Windows.Forms.MessageBox]::Show(('{0} default click handler!' -f $sender.Text))
      })

  }
  $button_ref.Value = $local:b
}

并重构代码以打包代码引用、菜单文本等。

#  Menu 3 button 3
# Provide a callback with  System.Windows.Forms.Button.OnClick Method argument signature
[scriptblock]$b3_3_callback_ref = {
  param(
    [object]$sender,
    [System.EventArgs]$eventargs
  )
  $caller.Data = 'something'
  [System.Windows.Forms.MessageBox]::Show(('This is custom callback for {0} click!' -f $sender.Text))
}

add_button -button_ref ([ref]$b3_3) `
   -button_data_ref ([ref]@{
    'cnt' = 3;
    'text' = 'Menu 3 Sub Menu 3';
    'name' = 'b3_3';
    'callback' = $b3_3_callback_ref;
  })

最终按钮数据对象和回调操作代码的布局当然高度依赖于领域。

选中组合框

下一个示例使用来自 带选中列表框作为下拉列表的组合框 的代码。与本文中的大多数示例不同,此脚本不使用 $caller 对象——CheckedComboBox 类本身有很多属性——而是通过引用将哈希对象传递给窗体来返回选定的文本数据。

$albums = @{

  'Ring Ring (1973)' = $false;
  'Waterloo (1974)' = $false;
  'ABBA (1975)' = $true;
  'Arrival (1976)' = $false;
  'The Album (1977)' = $true;
  'Voulez-Vous (1979)' = $false;
  'Super Trouper (1980)' = $false;
  'The Visitors (1981)' = $false;
}

PromptCheckedCombo -Title 'Checked ComboBox  Sample Project' -data_ref ([ref]$albums)
Write-Output ('Result is: {0}' -f $caller.Message)
$albums

这里函数的签名是

function PromptCheckedCombo {

  param(
    [string]$title,
    [System.Management.Automation.PSReference]$data_ref  
)
...
  $ccb = New-Object -TypeName 'CheckComboBoxTest.CheckedComboBox'

  $data = $data_ref.Value
  $cnt = 0
  $data.Keys | ForEach-Object { $display_item = $_;

    [CheckComboBoxTest.CCBoxItem]$item = New-Object CheckComboBoxTest.CCBoxItem ($display_item,$cnt)
    $ccb.Items.Add($item) | Out-Null
    if ($data[$display_item]) {
      $ccb.SetItemChecked($cnt,$true)
    }
    $cnt++
  }

result

在窗体委托中,迭代引用数据的键,并清除/设置哈希值。

  $eventMethod_ccb = $ccb.add_DropDownClosed
  $eventMethod_ccb.Invoke({
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )

      $data = $data_ref.Value
      $data.Keys | ForEach-Object {
        $display_item = $_;
        $data_ref.Value[$display_item] = $false
      }
      foreach ($item in $ccb.CheckedItems) {
        $data_ref.Value[$item.Name] = $true
      }

      $data_ref.Value = $data

    })

result

条形图

下一个示例显示了自定义绘制的条形图,它没有第三方图表库依赖。使用了来自 Drawing a Bar Chart 文章的 VB.NET 示例代码,并进行了一些小的重构和修改。

Add-Type -Language 'VisualBasic' -TypeDefinition  @"
Imports Microsoft.VisualBasic
Imports System
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Collections
Imports System.Windows.Forms

Public Class BarChart

    Inherits System.Windows.Forms.Form

    Public Sub New()
        MyBase.New()
        InitializeComponent()
    End Sub

    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub

    Private components As System.ComponentModel.IContainer

    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
        Me.ClientSize = New System.Drawing.Size(344, 302)
        Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable
        Me.Name = "BarChart"
        Me.Text = "BarChart"
        Me.components = New System.ComponentModel.Container
        Me.ttHint = New System.Windows.Forms.ToolTip(Me.components)
    End Sub

    Dim blnFormLoaded As Boolean = False
    Dim objHashTableG As New Hashtable(100)

    Dim objColorArray(150) As Brush
    Private Sub BarChart_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    End Sub

    Public Sub LoadData(ByVal objCallerHashTable As Hashtable )
       objHashTableG =  objCallerHashTable.Clone()
    End Sub


    Public Sub RenderData 
        Me.BarChart_Paint(Nothing, New System.Windows.Forms.PaintEventArgs( _
        CreateGraphics(), _
	New System.Drawing.Rectangle(0, 0, Me.Width, Me.Height) _
        )) 
    End Sub

    Private Sub BarChart_Paint(ByVal sender As Object, _
                               ByVal e As System.Windows.Forms.PaintEventArgs _
                               ) Handles MyBase.Paint
        Try
            Dim intMaxWidth As Integer
            Dim intMaxHeight As Integer
            Dim intXaxis As Integer
            Dim intYaxis As Integer
            Me.SuspendLayout()
            Me.LoadColorArray()
            intMaxHeight = CType((Me.Height / 2) - (Me.Height / 12), Integer)
            intMaxWidth = CType(Me.Width - (Me.Width / 4), Integer)
            intXaxis = CType(Me.Width / 12, Integer)
            intYaxis = CType(Me.Height / 2, Integer)
            drawBarChart(objHashTableG.GetEnumerator , _
                         objHashTableG.Count, _
                         "Graph 1", _
                         intXaxis, _
                         intYaxis, _
                         intMaxWidth, _
                         intMaxHeight, _
                         True, _
                         False)
            blnFormLoaded = True
            Me.ResumeLayout(False)
        Catch ex As Exception
            Throw ex
        End Try
        
    End Sub

    Public Sub drawBarChart(ByVal objEnum As IDictionaryEnumerator, _
                            ByVal intItemCount As Integer, _
                            ByVal strGraphTitle As String, _
                            ByVal Xaxis As Integer, _
                            ByVal Yaxis As Integer, _
                            ByVal MaxWidth As Int16, _
                            ByVal MaxHt As Int16, _
                            ByVal clearForm As Boolean, _
                            Optional ByVal SpaceRequired As Boolean = False)

        Dim intGraphXaxis As Integer = Xaxis
        Dim intGraphYaxis As Integer = Yaxis
        Dim intWidthMax As Integer = MaxWidth
        Dim intHeightMax As Integer = MaxHt
        Dim intSpaceHeight As Integer
        Dim intMaxValue As Integer = 0
        Dim intCounter As Integer
        Dim intBarWidthMax
        Dim intBarHeight
        Dim strText As String
        Try
            Dim grfx As Graphics = CreateGraphics()
            If clearForm = True Then
                grfx.Clear(BackColor)
            End If

            grfx.DrawString(strGraphTitle, New Font("Verdana", 12.0, FontStyle.Bold, GraphicsUnit.Point), Brushes.DeepPink, intGraphXaxis + (intWidthMax / 4), (intGraphYaxis - intHeightMax) - 40)

            'Get the Height of the Bar        
            intBarHeight = CInt(intHeightMax / intItemCount)

            'Get the space Height of the Bar 
            intSpaceHeight = CInt((intHeightMax / (intItemCount - 1)) - intBarHeight)

            'Find Maximum of the input value
            If Not objEnum Is Nothing Then
                While objEnum.MoveNext = True
                    If objEnum.Value > intMaxValue Then
                        intMaxValue = objEnum.Value
                    End If
                End While
            End If

            'Get the Maximum Width of the Bar
            intBarWidthMax = CInt(intWidthMax / intMaxValue)

            ' Obtain the Graphics object exposed by the Form.
            If Not objEnum Is Nothing Then
                intCounter = 1
                objEnum.Reset()
                'Draw X axis and Y axis lines
                'grfx.DrawLine(Pens.Black, intGraphXaxis, intGraphYaxis, intGraphXaxis + intWidthMax, intGraphYaxis)
                'grfx.DrawLine(Pens.Black, intGraphXaxis, intGraphYaxis, intGraphXaxis, (intGraphYaxis - intHeightMax) - 25)

                While objEnum.MoveNext = True
                    'Get new Y axis
                    intGraphYaxis = intGraphYaxis - intBarHeight

                    Dim objRec as Rectangle
                    objRec = New System.Drawing.Rectangle(intGraphXaxis, intGraphYaxis, intBarWidthMax * objEnum.Value, intBarHeight)
                    'Draw Rectangle
                    grfx.DrawRectangle(Pens.Black, objRec)
                    'Fill Rectangle
                    grfx.FillRectangle(objColorArray(intCounter), objRec )
                    'Display Text and value
                    ' http://www.java2s.com/Tutorial/VB/0300__2D-Graphics/Measurestringanddrawstring.htm
                    strText =  objEnum.Key & "=" & objEnum.Value 
                    Dim objLabelFont as Font
                    objLabelFont = New Font("Verdana", 7.2, FontStyle.Regular, GraphicsUnit.Point) 
                    Dim textLabelArea As SizeF : textLabelArea = grfx.MeasureString(strText, objLabelFont)

                    Dim linePen As Pen: linePen = New Pen(Color.Gray, 1)
                    linePen.DashStyle = Drawing2D.DashStyle.Dash

                    Dim fontRatio As Single 
                    fontRatio = objLabelFont.Height / objLabelFont.FontFamily.GetLineSpacing(FontStyle.Regular)
 
                    Dim ascentSize As Single
                    ascentSize = objLabelFont.FontFamily.GetCellAscent(FontStyle.Regular) * fontRatio
                    Dim descentSize As Single
                    descentSize = objLabelFont.FontFamily.GetCellDescent(FontStyle.Regular) * fontRatio
                    Dim emSize As Single
                    emSize = objLabelFont.FontFamily.GetEmHeight(FontStyle.Regular) * fontRatio
                    Dim cellHeight As Single
                    cellHeight = ascentSize + descentSize
                    Dim internalLeading As Single
                    internalLeading = cellHeight - emSize
                    Dim externalLeading As Single
                    externalLeading = (objLabelFont.FontFamily.GetLineSpacing(FontStyle.Regular) * fontRatio) - cellHeight


                    Dim labelLeft As Single : labelLeft = intGraphXaxis + (intBarWidthMax * objEnum.Value)
                    labelLeft = intGraphXaxis
                    Dim labelBottom As Single:  labelBottom =  intGraphYaxis
                    Dim labelRight As Single : labelRight = labelLeft + textLabelArea.Width
                    Dim labelTop As Single : labelTop = textLabelArea.Height + labelBottom

                    Dim objLabelRec as Rectangle
                    objLabelRec = New System.Drawing.Rectangle(labelLeft, labelBottom, textLabelArea.Width , textLabelArea.Height )
                 
                    grfx.DrawRectangle(Pens.Black, objLabelRec)
                    'Fill Rectangle
                    grfx.FillRectangle(Brushes.White, objLabelRec )

                    grfx.DrawLine(linePen, labelLeft, labelTop, labelLeft , labelBottom)
                    grfx.DrawLine(linePen, labelRight, labelTop, labelRight , labelBottom)
                    grfx.DrawLine(linePen, labelLeft, labelTop, labelRight , labelTop)
                    grfx.DrawLine(linePen, labelLeft, labelBottom, labelRight , labelBottom)
                    grfx.DrawString(strText, objLabelFont, Brushes.Black, labelLeft, labelBottom)

                    intCounter += 1
                    If SpaceRequired = True Then
                        intGraphYaxis = intGraphYaxis - intSpaceHeight
                    End If
                    If intCounter > objColorArray.GetUpperBound(0) Then
                        intCounter = 1
                    End If
                End While
                If clearForm = True Then
                    grfx.Dispose()
                End If
            End If
        Catch ex As Exception
            Throw ex
        End Try
    End Sub

    Public Sub LoadColorArray()
        objColorArray(1) = Brushes.Blue
        objColorArray(2) = Brushes.Pink
        objColorArray(3) = Brushes.Brown
        objColorArray(4) = Brushes.BurlyWood
        objColorArray(5) = Brushes.CadetBlue
        objColorArray(6) = Brushes.Chartreuse
        objColorArray(7) = Brushes.Chocolate
        objColorArray(8) = Brushes.Coral
        objColorArray(9) = Brushes.CornflowerBlue
        objColorArray(10) = Brushes.Cornsilk
        objColorArray(11) = Brushes.Crimson
        objColorArray(12) = Brushes.Cyan
        objColorArray(13) = Brushes.DarkBlue
        objColorArray(14) = Brushes.DarkCyan
        objColorArray(15) = Brushes.DarkGoldenrod
        objColorArray(16) = Brushes.DarkGray
        objColorArray(17) = Brushes.DarkGreen
        objColorArray(18) = Brushes.DarkKhaki
        objColorArray(19) = Brushes.DarkMagenta
        objColorArray(20) = Brushes.DarkOliveGreen
        objColorArray(21) = Brushes.DarkOrange
        objColorArray(22) = Brushes.DarkOrchid
        objColorArray(23) = Brushes.DarkRed
        objColorArray(24) = Brushes.DarkSalmon
        objColorArray(25) = Brushes.DarkSeaGreen
        objColorArray(26) = Brushes.DarkSlateBlue
        objColorArray(27) = Brushes.DarkSlateGray
        objColorArray(28) = Brushes.DarkTurquoise
        objColorArray(29) = Brushes.DarkViolet
        objColorArray(30) = Brushes.DeepPink
        objColorArray(31) = Brushes.DeepSkyBlue
        objColorArray(32) = Brushes.DimGray
        objColorArray(33) = Brushes.DodgerBlue
        objColorArray(34) = Brushes.Firebrick
        objColorArray(35) = Brushes.FloralWhite
        objColorArray(36) = Brushes.ForestGreen
        objColorArray(37) = Brushes.Fuchsia
        objColorArray(38) = Brushes.Gainsboro
        objColorArray(39) = Brushes.GhostWhite
        objColorArray(40) = Brushes.Gold
        objColorArray(41) = Brushes.Goldenrod
        objColorArray(42) = Brushes.Gray
        objColorArray(43) = Brushes.Green
        objColorArray(44) = Brushes.GreenYellow
        objColorArray(45) = Brushes.Honeydew
        objColorArray(46) = Brushes.HotPink
        objColorArray(47) = Brushes.IndianRed
        objColorArray(48) = Brushes.Indigo
        objColorArray(49) = Brushes.Ivory
        objColorArray(50) = Brushes.Khaki
        objColorArray(51) = Brushes.Lavender
        objColorArray(52) = Brushes.LavenderBlush
        objColorArray(53) = Brushes.LawnGreen
        objColorArray(54) = Brushes.LemonChiffon
        objColorArray(55) = Brushes.LightBlue
        objColorArray(56) = Brushes.LightCoral
        objColorArray(57) = Brushes.LightCyan
        objColorArray(58) = Brushes.LightGoldenrodYellow
        objColorArray(59) = Brushes.LightGray
        objColorArray(60) = Brushes.LightGreen
        objColorArray(61) = Brushes.LightPink
        objColorArray(62) = Brushes.LightSalmon
        objColorArray(63) = Brushes.LightSeaGreen
        objColorArray(64) = Brushes.LightSkyBlue
        objColorArray(65) = Brushes.LightSlateGray
        objColorArray(66) = Brushes.LightSteelBlue
        objColorArray(67) = Brushes.LightYellow
        objColorArray(68) = Brushes.Lime
        objColorArray(69) = Brushes.LimeGreen
        objColorArray(70) = Brushes.Linen
        objColorArray(71) = Brushes.Magenta
        objColorArray(72) = Brushes.Maroon
        objColorArray(73) = Brushes.MediumAquamarine
        objColorArray(74) = Brushes.MediumBlue
        objColorArray(75) = Brushes.MediumOrchid
        objColorArray(76) = Brushes.MediumPurple
        objColorArray(77) = Brushes.MediumSeaGreen
        objColorArray(78) = Brushes.MediumSlateBlue
        objColorArray(79) = Brushes.MediumSpringGreen
        objColorArray(80) = Brushes.MediumTurquoise
        objColorArray(81) = Brushes.MediumVioletRed
        objColorArray(82) = Brushes.MidnightBlue
        objColorArray(83) = Brushes.MintCream
        objColorArray(84) = Brushes.MistyRose
        objColorArray(85) = Brushes.Moccasin
        objColorArray(86) = Brushes.NavajoWhite
        objColorArray(87) = Brushes.Navy
        objColorArray(88) = Brushes.OldLace
        objColorArray(89) = Brushes.Olive
        objColorArray(90) = Brushes.OliveDrab
        objColorArray(91) = Brushes.Orange
        objColorArray(92) = Brushes.OrangeRed
        objColorArray(93) = Brushes.Orchid
        objColorArray(94) = Brushes.PaleGoldenrod
        objColorArray(95) = Brushes.PaleGreen
        objColorArray(96) = Brushes.PaleTurquoise
        objColorArray(97) = Brushes.PaleVioletRed
        objColorArray(98) = Brushes.PapayaWhip
        objColorArray(99) = Brushes.PeachPuff
        objColorArray(100) = Brushes.Peru
        objColorArray(101) = Brushes.Pink
        objColorArray(102) = Brushes.Plum
        objColorArray(103) = Brushes.PowderBlue
        objColorArray(104) = Brushes.Purple
        objColorArray(105) = Brushes.Red
        objColorArray(106) = Brushes.RosyBrown
        objColorArray(107) = Brushes.RoyalBlue
        objColorArray(108) = Brushes.SaddleBrown
        objColorArray(109) = Brushes.Salmon
        objColorArray(110) = Brushes.SandyBrown
        objColorArray(111) = Brushes.SeaGreen
        objColorArray(112) = Brushes.SeaShell
        objColorArray(113) = Brushes.Sienna
        objColorArray(114) = Brushes.Silver
        objColorArray(115) = Brushes.SkyBlue
        objColorArray(116) = Brushes.SlateBlue
        objColorArray(117) = Brushes.SlateGray
        objColorArray(118) = Brushes.Snow
        objColorArray(119) = Brushes.SpringGreen
        objColorArray(120) = Brushes.SteelBlue
        objColorArray(121) = Brushes.Tan
        objColorArray(122) = Brushes.Teal
        objColorArray(123) = Brushes.Thistle
        objColorArray(124) = Brushes.Tomato
        objColorArray(125) = Brushes.Transparent
        objColorArray(126) = Brushes.Turquoise
        objColorArray(127) = Brushes.Violet
        objColorArray(128) = Brushes.Wheat
        objColorArray(129) = Brushes.White
        objColorArray(130) = Brushes.WhiteSmoke
        objColorArray(131) = Brushes.Yellow
        objColorArray(132) = Brushes.YellowGreen
    End Sub
    Private Sub BarChart_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Resize
        If blnFormLoaded = True Then
            BarChart_Paint(Me, New System.Windows.Forms.PaintEventArgs(CreateGraphics(), New System.Drawing.Rectangle(0, 0, Me.Width, Me.Height)))
        End If
    End Sub
    Friend WithEvents ttHint As System.Windows.Forms.ToolTip
    ' Friend WithEvents RecLabel As System.Windows.Forms.Label
    '' need to draw System.Windows.Forms.Control
End Class
"@ -ReferencedAssemblies 'System.Windows.Forms.dll', 'System.Drawing.dll', 'System.Drawing.dll'

在此演示中,PowerShell 打开窗体并将两个数据样本发送给它,在每个样本渲染后等待几秒钟,然后关闭窗体。

grid

$object = New-Object -TypeName 'BarChart'

$data1 = New-Object System.Collections.Hashtable(10)
$data1.Add("Product1", 25)
$data1.Add("Product2", 15)
$data1.Add("Product3", 35)
$object.LoadData([System.Collections.Hashtable] $data1)

[void]$object.Show()
start-sleep -seconds 5

$data2 =  New-Object System.Collections.Hashtable(100)
$data2.Add("Item1", 50)
$data2.Add("Item2", 150)
$data2.Add("Item3", 250)
$data2.Add("Item4", 20)
$data2.Add("Item5", 100)
$data2.Add("Item6", 125)
$data2.Add("Item7", 148)
$data2.Add("Item8", 199)
$data2.Add("Item9", 267)

$object.LoadData([System.Collections.Hashtable] $data2)

$object.RenderData()
start-sleep -seconds 5

$object.Close()
$object.Dispose()

已向该窗体添加了两个公共方法 LoadDataRenderData 以允许从脚本控制窗体。为了防止修改原始示例,第一个方法克隆了来自调用者的数据,而后者创建了一个虚拟的事件参数并调用了处理程序。

    Public Sub LoadData(ByVal objCallerHashTable As Hashtable )
       objHashTableG =  objCallerHashTable.Clone()
    End Sub


    Public Sub RenderData 
        Me.BarChart_Paint(Nothing, New System.Windows.Forms.PaintEventArgs( _
        CreateGraphics(), _
	New System.Drawing.Rectangle(0, 0, Me.Width, Me.Height) _
        )) 
    End Sub

没有从窗体到脚本的通信,因此不需要实现 IWin32Window 的单独对象。为了举例,下面仍然提供了 VB.Net 版本。

Add-Type -Language 'VisualBasic' -TypeDefinition @"
Public Class MyWin32Window 
Implements System.Windows.Forms.IWin32Window

    Dim _hWnd As System.IntPtr

    Public Sub New(ByVal handle As System.IntPtr)

       _hWnd = handle

    End Sub

    Public ReadOnly Property Handle() As System.IntPtr Implements System.Windows.Forms.IWin32Window.Handle
        Get
            Handle = _hWnd
        End Get
    End Property
 
End Class
"@ -ReferencedAssemblies 'System.Windows.Forms.dll'
$caller = New-Object -TypeName 'MyWin32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

图表的真实世界数据

为了给条形图(或甘特图)提供真实世界的数据样本,您可能希望捕获网站页面元素的加载时间以进行性能测量。这很容易借助 FiddlerCore 程序集完成,如下所示。脚本的 c# 部分包含一个修改过的 fiddlercore-demo 示例,重点关注 Fiddler 提供的部分指标。

Add-Type @"
using System;
using Fiddler;

namespace WebTester
{
    public class Monitor
    {
        public Monitor()
        {
            #region AttachEventListeners

            // Simply echo notifications to the console.  Because CONFIG.QuietMode=true 
            // by default, we must handle notifying the user ourselves.
            FiddlerApplication.OnNotification += delegate(object sender, NotificationEventArgs oNEA) { Console.WriteLine("** NotifyUser: " + oNEA.NotifyString); };
            FiddlerApplication.Log.OnLogString += delegate(object sender, LogEventArgs oLEA) { Console.WriteLine("** LogString: " + oLEA.LogString); };

            FiddlerApplication.BeforeRequest += (s) =>
            {
                // In order to enable response tampering, buffering mode must
                // be enabled; this allows FiddlerCore to permit modification of
                // the response in the BeforeResponse handler rather than streaming
                // the response to the client as the response comes in.
                s.bBufferResponse = true;
            };

            FiddlerApplication.BeforeResponse += (s) =>
            {
                // Uncomment the following to decompress/unchunk the HTTP response 
                // s.utilDecodeResponse(); 
            };


            FiddlerApplication.AfterSessionComplete += (fiddler_session) =>
            {
                // Ignore HTTPS connect requests
                if (fiddler_session.RequestMethod == "CONNECT")
                    return;

                if (fiddler_session == null || fiddler_session.oRequest == null || fiddler_session.oRequest.headers == null)
                    return;

                var full_url = fiddler_session.fullUrl;
                Console.WriteLine("URL: " + full_url);

                HTTPResponseHeaders response_headers = fiddler_session.ResponseHeaders;
                Console.WriteLine("HTTP Response: " + response_headers.HTTPResponseCode.ToString());
                /*
                foreach (HTTPHeaderItem header_item in response_headers){
                   Console.WriteLine(header_item.Name + " " + header_item.Value);
                }
                    */
                // http://fiddler.wikidot.com/timers
                var timers = fiddler_session.Timers;
                var duration = timers.ClientDoneResponse - timers.ClientBeginRequest;
                Console.WriteLine(String.Format("Duration: {0:F10}", duration.Milliseconds));

            };
            #endregion AttachEventListeners
        }


        public void Start()
        {
            Console.WriteLine("Starting FiddlerCore...");
            // For the purposes of this demo, we'll forbid connections to HTTPS 
            // sites that use invalid certificates
            CONFIG.IgnoreServerCertErrors = false;
            // Because we've chosen to decrypt HTTPS traffic, makecert.exe must
            // be present in the Application folder.
            FiddlerApplication.Startup(8877, true, true);
            Console.WriteLine("Hit CTRL+C to end session.");
            // Wait Forever for the user to hit CTRL+C.  
            // BUG BUG: Doesn't properly handle shutdown of Windows, etc.
        }

        public void Stop()
        {
            Console.WriteLine("Shutdown.");
            FiddlerApplication.Shutdown();
            System.Threading.Thread.Sleep(1);
        }
        public static Monitor m;

        static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
        {
            Console.WriteLine("Stop.");
            m.Stop();
            System.Threading.Thread.Sleep(1);
        }
    }
}
"@ -ReferencedAssemblies 'System.dll','System.Data.dll',"${shared_assemblies_path}\FiddlerCore4.dll"

修改主要是在 AfterSessionComplete 委托中进行的。这个类嵌入在 PowerShell 中,并设置为监听大约 $selenium.Navigate().GoToUrl($base_url) 调用期间的流量。

$o = New-Object -TypeName 'WebTester.Monitor'
$o.Start()
# ...  initialize $selenium ...
$selenium.Navigate().GoToUrl($base_url)
$o.Stop()
[bool]$fullstop = [bool]$PSBoundParameters['pause'].IsPresent

收集持续时间的替代方法是简单地通过 Selenium 调用 Chrome 浏览器中的 Javascript。

using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Remote;

namespace WebTester
{

        // http://stackoverflow.com/questions/6229769/execute-javascript-using-selenium-webdriver-in-c-sharp
        // http://stackoverflow.com/questions/14146513/selenium-web-driver-c-sharp-invalidcastexception-for-list-of-webelements-after-j
        // http://stackoverflow.com/questions/8133661/checking-page-load-time-of-several-url-simultaneously
        // http://blogs.msdn.com/b/fiddler/archive/2011/02/10/fiddler-is-better-with-internet-explorer-9.aspx

    public static class Extensions
    {
        static int cnt = 0;

        public static T Execute(this IWebDriver driver, string script)
        {
            return (T)((IJavaScriptExecutor)driver).ExecuteScript(script);
        }

		        // http://stackoverflow.com/questions/6229769/execute-javascript-using-selenium-webdriver-in-c-sharp
        // http://stackoverflow.com/questions/14146513/selenium-web-driver-c-sharp-invalidcastexception-for-list-of-webelements-after-j
        // http://stackoverflow.com/questions/8133661/checking-page-load-time-of-several-url-simultaneously
        // http://blogs.msdn.com/b/fiddler/archive/2011/02/10/fiddler-is-better-with-internet-explorer-9.aspx

        public static List> Performance(this IWebDriver driver)
        {
            // NOTE: performance.getEntries is only with Chrome
            // performance.timing is available for FF and PhantomJS

            string performance_script = @"
var ua = window.navigator.userAgent;

if (ua.match(/PhantomJS/)) {
    return 'Cannot measure on ' + ua;
} else {
    var performance =
        window.performance ||
        window.mozPerformance ||
        window.msPerformance ||
        window.webkitPerformance || {};

    // var timings = performance.timing || {};
    // return timings;
    var network = performance.getEntries() || {};
    return network;
}
";
            List> result = new List>();
            IEnumerable<Object> raw_data = driver.Execute>(performance_script);

            foreach (var element in (IEnumerable<Object>)raw_data)
            {
                Dictionary row = new Dictionary();
                Dictionary dic = (Dictionary)element;
                foreach (object key in dic.Keys)
                {
                    Object val = null;
                    if (!dic.TryGetValue(key.ToString(), out val)) { val = ""; }
                    row.Add(key.ToString(), val.ToString());
                }
                result.Add(row);
            }
            return result;
        }

public static void WaitDocumentReadyState(
/* this // no longer is an extension method  */ 
IWebDriver driver, string expected_state, int max_cnt = 10)
        {
            cnt = 0;
            var wait = new OpenQA.Selenium.Support.UI.WebDriverWait(driver, TimeSpan.FromSeconds(30.00));
            wait.PollingInterval = TimeSpan.FromSeconds(0.50);
            wait.Until(dummy =>
            {
                string result = driver.Execute("return document.readyState").ToString();
                Console.Error.WriteLine(String.Format("result = {0}", result));
                Console.WriteLine(String.Format("cnt = {0}", cnt));
                cnt++;
                // TODO: match
                return ((result.Equals(expected_state) || cnt > max_cnt));
            });
        }
    }

}

$selenium.Navigate().GoToUrl($base_url)
$expected_states = @( "interactive", "complete" );
[WebTester.Extensions]::WaitDocumentReadyState($selenium, $expected_states[1])
$script = @"
var ua = window.navigator.userAgent;

if (ua.match(/PhantomJS/)) { 
return 'Cannot measure on '+ ua;
}
else{
var performance = 
      window.performance || 
      window.mozPerformance || 
      window.msPerformance || 
      window.webkitPerformance || {}; 
// var timings = performance.timing || {};
// return timings;
// NOTE:  performance.timing will not return anything with Chrome
// timing is returned by FF
// timing is returned by Phantom
var network = performance.getEntries() || {}; 
 return network;
}


"@

# executeScript works fine with Chrome or Firefox 31, ie 10, but not IE 11.
# Exception calling "ExecuteScript" with "1" argument(s): "Unable to get browser
# https://code.google.com/p/selenium/issues/detail?id=6511  
# 
# https://code.google.com/p/selenium/source/browse/java/client/src/org/openqa/selenium/remote/HttpCommandExecutor.java?r=3f4622ced689d2670851b74dac0c556bcae2d0fe

$savedata = $true

if ($headless) {
  # for PhantomJS more work is needed
  # https://github.com/detro/ghostdriver/blob/master/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSDriver.java
  $result = ([OpenQA.Selenium.PhantomJS.PhantomJSDriver]$selenium).ExecutePhantomJS($script,[System.Object[]]@())
  $result | Format-List
  return
} else {

  $result = ([OpenQA.Selenium.IJavaScriptExecutor]$selenium).executeScript($script)
  # $result | get-member 

  $result | ForEach-Object {
    $element_result = $_
    # $element_result | format-list
    Write-Output $element_result.Name
    Write-Output $element_result.duration
    $o = New-Object PSObject
    $caption = 'test'
    $o | Add-Member Noteproperty 'url' $element_result.Name
    $o | Add-Member Noteproperty 'caption' $caption
    $o | Add-Member Noteproperty 'load_time' $element_result.duration
    $o | Format-List

    if ($savedata) {
      insert_database3 -data $o -database "$script_directory\timings.db"
    }
    $o = $null

完整的脚本可在附加的 zip 文件中找到。

折线图、条形图和饼图

下一个示例展示了另一个自定义绘制的 折线图、条形图和饼图库,它也实现在一个 C# 类中。

result

Add-Type @"

// "
 #region PROGRAM HEADER
/*
*********************************************************************************************
* FILE NAME      : DrawGraph.cs                                                             *
* DESCRIPTION    : Generates Bar, Line & Pie graph for a set of values [maximum limit= 10]  *
* AUTHOR         : Anoop Unnikrishnan   (AUK) 
// ... currently we use unmodified code ...


"@ -ReferencedAssemblies 'System.Windows.Forms.dll','System.Drawing.dll','System.Data.dll','System.Xml.dll'

窗体仅限于选择图表形状。请注意,中还有更多形状(此处未显示)。

function DrawGraph {

  param(
    [string]$title,
    [System.Management.Automation.PSReference]$data_ref,
    [object]$caller
  )

  @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title


  $f.Size = New-Object System.Drawing.Size (470,385)
  $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
  $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
  $f.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
  $f.SuspendLayout()

  $o = New-Object -TypeName 'System.Anoop.Graph.DrawGraph' -ArgumentList @( [string[]]$data_ref.Value.Keys,
    [float[]]$data_ref.Value.Values,
    $null,
    $null,
    'Arial',
    200
  )
  [System.Windows.Forms.PictureBox]$b = New-Object -TypeName 'System.Windows.Forms.PictureBox'
  $b.Location = New-Object System.Drawing.Point (40,20)
  $b.Name = 'p5'
  $b.Size = New-Object System.Drawing.Size (($f.Size.Width - 20),($f.Size.Height - 100))
  $b.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::AutoSize
  $b.TabIndex = 1
  $b.TabStop = $false

  $m = New-Object -TypeName 'System.Windows.Forms.MenuStrip'
  $file_m1 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
  $shape_m1 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
  $shape_m2 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
  $shape_m3 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
  $exit_m1 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
  $m.SuspendLayout()

  #  m0
  $m.Items.AddRange(@( $file_m1,$exit_m1))
  $m.Location = New-Object System.Drawing.Point (0,0)
  $m.Name = "m0"
  $m.Size = New-Object System.Drawing.Size (($f.Size.Width),24)
  $m.TabIndex = 0
  $m.Text = "m0"


  #  ShapeToolStripMenuItem
  $shape_m1.Name = "LineGraphToolStripMenuItem"
  $shape_m1.Text = "Line Graph"

  $eventMethod_shape_m1 = $shape_m1.add_click
  $eventMethod_shape_m1.Invoke({
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
      $who = $sender.Text
      # [System.Windows.Forms.MessageBox]::Show(("We are processing {0}." -f $who))
      $b.Image = $o.DrawLineGraph()
      $caller.Data = $sender.Text
    })

  $shape_m2.Name = "BarGraphToolStripMenuItem"
  $shape_m2.Text = "Bar Graph"

  $eventMethod_shape_m2 = $shape_m2.add_click
  $eventMethod_shape_m2.Invoke({
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
      $who = $sender.Text
      # [System.Windows.Forms.MessageBox]::Show(("We are processing {0}." -f $who))
      $b.Image = $o.DrawBarGraph()
      $caller.Data = $sender.Text
    })

  $shape_m3.Name = "3dPieChartToolStripMenuItem"
  $shape_m3.Text = "3d Pie Chart"

  $eventMethod_shape_m3 = $shape_m3.add_click
  $eventMethod_shape_m3.Invoke({
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
      $who = $sender.Text
      # [System.Windows.Forms.MessageBox]::Show(("We are processing {0}." -f $who))
      $b.Image = $o.Draw3DPieGraph()
      $caller.Data = $sender.Text
    })

  # Separator 
  $dash = New-Object -TypeName System.Windows.Forms.ToolStripSeparator

  #  exitToolStripMenuItem
  $exit_m1.Name = "exitToolStripMenuItem"
  $exit_m1.Text = "Exit"

  $eventMethod_exit_m1 = $exit_m1.add_click
  $eventMethod_exit_m1.Invoke({
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
      $who = $sender.Text
      # [System.Windows.Forms.MessageBox]::Show(("We are processing {0}." -f $who))
      $caller.Data = $sender.Text
      $f.Close()
    })

  #  fileToolStripMenuItem1
  $file_m1.DropDownItems.AddRange(@( $shape_m1, $shape_m2, $dash, $shape_m3))
  $file_m1.Name = "DrawToolStripMenuItem1"
  $file_m1.Text = "Draw"

  $m.ResumeLayout($false)

  #  MenuTest
  $f.AutoScaleDimensions = New-Object System.Drawing.SizeF (1,1)
  $f.Controls.AddRange(@( $m,$b))
  $f.Topmost = $True

  $f.Add_Shown({ $f.Activate() })

  [void]$f.ShowDialog([win32window]($caller))

  $f.Dispose()
}

result

调用者通过引用传递数据。

$caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
$data = @{
  "USA" = 10;
  "UK" = 30;
  "Japan" = 60;
  "China" = 40;
  "Bhutan" = 5;
  "India" = 60; 
}
[void](DrawGraph -Title $title -caller $caller -data_ref ([ref]$data))

数据网格概念验证

grid

网格无疑是提供给用户操作的最复杂对象。

function PromptGrid(
    [System.Collections.IList] $data,
    [Object] $caller = $null
    ){

  if ($caller -eq $null ){
    $caller = New-Object Win32Window -ArgumentList
    ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
  }

[System.Reflection.Assembly]::LoadWithPartiaName('System.Windows.Forms') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('System.Data') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')  | out-null

$f = New-Object System.Windows.Forms.Form
$f.Text = 'how do we open these stones? '
$f.AutoSize  = $true
$grid = New-Object System.Windows.Forms.DataGrid
$grid.PreferredColumnWidth = 100

$System_Drawing_Size = New-Object System.Drawing.Size
$grid.DataBindings.DefaultDataSourceUpdateMode = 0
$grid.HeaderForeColor = [System.Drawing.Color]::FromArgb(255,0,0,0)

$grid.Name = "dataGrid1"
$grid.DataMember = ''
$grid.TabIndex = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 13;
$System_Drawing_Point.Y = 48 ;
$grid.Location = $System_Drawing_Point
$grid.Dock = [System.Windows.Forms.DockStyle]::Fill

$button = New-Object System.Windows.Forms.Button
$button.Text = 'Open'
$button.Dock = [System.Windows.Forms.DockStyle]::Bottom

$f.Controls.Add( $button )
$f.Controls.Add( $grid )

$button.add_Click({
# http://msdn.microsoft.com/en-us/library/system.windows.forms.datagridviewrow.cells%28v=vs.110%29.aspx

if ($grid.IsSelected(0)){
 $caller.Data = 42;
}
$f.Close()

})

$grid.DataSource =  $data
$f.ShowDialog([Win32Window ] ($caller)) | out-null
$f.Topmost = $True
$f.refresh()
$f.Dispose()
}

function display_result{
param ([Object] $result)

$array = New-Object System.Collections.ArrayList

foreach ($key in $result.keys){
  $value = $result[$key]
  $o = New-Object PSObject
  $o | add-member Noteproperty 'Substance'  $value[0]
  $o | add-member Noteproperty 'Action' $value[1]
  $array.Add($o)
}

$process_window = New-Object Win32Window -ArgumentList
([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
$ret = (PromptGrid $array $process_window)
}

$data = @{ 1 = @('wind', 'blows...');
           2 = @('fire',  'burns...');
           3 = @('water',  'falls...')
        }

display_result $data

这里,事件处理程序暂时留给读者练习——它可能高度依赖于领域。请访问作者的github 存储库以获取此脚本的更新。

例如,可以使用 GridListView 提示用户输入缺失的参数。如果脚本参数是

[CmdletBinding()]param ( [string] $string_param1 = '' ,
        [string] $string_param2 = '' ,
        [string] $string_param3 = '' ,
        [boolean] $boolean_param = $false,
        [int] $int_param
    )

并且调用仅传递了部分参数,则可以使用以下代码片段来发现参数状态。

[CmdletBinding()]# Get the command name
$CommandName = $PSCmdlet.MyInvocation.InvocationName

# Get the list of parameters for the command
$ParameterList = (Get-Command -Name $CommandName).Parameters
$parameters = @{}
foreach ($Parameter in $ParameterList) {
  # Grab each parameter value, using Get-Variable
  $value = Get-Variable -Name $Parameter.Values.Name -ErrorAction SilentlyContinue
}

然后填充 $parameters Hashtable 并将其传递给 Form

$parameters = @{ }
$value | foreach-object {$parameters[$_.Name] = $_.Value }
$caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
Edit_Parameters -parameters ($parameters) -caller $caller -title 'Provide parameters: '

其定义如下:

function Edit_Parameters {
     Param(
    [Hashtable] $parameters,
        [String] $title,
    [Object] $caller= $null
    )

  [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | out-null
  [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel') | out-null
  [System.Reflection.Assembly]::LoadWithPartialName('System.Data') | out-null
  [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')  | out-null

  $f = New-Object System.Windows.Forms.Form
  $f.SuspendLayout();

  $f.Text = $title
  $f.AutoSize  = $true
  $grid = New-Object System.Windows.Forms.DataGridView
  $grid.Autosize = $true
  $grid.DataBindings.DefaultDataSourceUpdateMode = 0

  $grid.Name = 'dataGrid1'
  $grid.DataMember = ''
  $grid.TabIndex = 0
  $grid.Location = new-object System.Drawing.Point(13,50)
  $grid.Dock = [System.Windows.Forms.DockStyle]::Fill

  $grid.ColumnCount = 2
  $grid.Columns[0].Name = 'Parameter Name'
  $grid.Columns[1].Name = 'Value'

  $parameters.Keys | foreach-object {
            $row1 = @( $_,  $parameters[$_].ToString())
            $grid.Rows.Add($row1)
  }

  $grid.Columns[0].ReadOnly = $true;

  foreach ($row in $grid.Rows){
             $row.cells[0].Style.BackColor = [System.Drawing.Color]::LightGray
             $row.cells[0].Style.ForeColor = [System.Drawing.Color]::White
             $row.cells[1].Style.Font = New-Object System.Drawing.Font('Lucida Console', 9)
      }

  $button = New-Object System.Windows.Forms.Button
  $button.Text = 'Run'
  $button.Dock = [System.Windows.Forms.DockStyle]::Bottom

  $f.Controls.Add( $button)
  $f.Controls.Add( $grid )
  $grid.ResumeLayout($false)
  $f.ResumeLayout($false)

  $button.add_Click({

    foreach ($row in $grid.Rows){
      # do not close the form if some parameters are not entered
      if (($row.cells[0].Value -ne $null -and $row.cells[0].Value -ne '' ) -and ($row.cells[1].Value -eq $null -or $row.cells[1].Value -eq '')) {
        $row.cells[0].Style.ForeColor = [System.Drawing.Color]::Red
        $grid.CurrentCell  = $row.cells[1]
        return;
      }
    }
      # TODO: return $caller.HashData
      # write-host ( '{0} = {1}' -f $row.cells[0].Value, $row.cells[1].Value.ToString())

    $f.Close()

  })

  $f.ShowDialog($caller) | out-null
  $f.Topmost = $True
  $f.refresh()
  $f.Dispose()
}

在按钮处理程序中,我们防止窗体关闭,直到没有空白参数为止。输入焦点会移到期望输入单元格。为简单起见,此处我们接受所有参数的文本输入,而不考虑其类型。

password

列表视图

现在假设您运行一系列松散的(例如 Selenium)测试,这些测试使用 Excel 文件作为测试参数和结果。

configuration

读取设置

$data_name = 'Servers.xls'
[string]$filename = ('{0}\{1}' -f (Get-ScriptDirectory),$data_name)

$sheet_name = 'ServerList$'
[string]$oledb_provider = 'Provider=Microsoft.Jet.OLEDB.4.0'
$data_source = "Data Source = $filename"
$ext_arg = "Extended Properties=Excel 8.0"
# TODO: hard coded id
[string]$query = "Select * from [${sheet_name}] where [id] <> 0"
[System.Data.OleDb.OleDbConnection]$connection = New-Object System.Data.OleDb.OleDbConnection ("$oledb_provider;$data_source;$ext_arg")
[System.Data.OleDb.OleDbCommand]$command = New-Object System.Data.OleDb.OleDbCommand ($query)


[System.Data.DataTable]$data_table = New-Object System.Data.DataTable
[System.Data.OleDb.OleDbDataAdapter]$ole_db_adapter = New-Object System.Data.OleDb.OleDbDataAdapter
$ole_db_adapter.SelectCommand = $command

$command.Connection = $connection
($rows = $ole_db_adapter.Fill($data_table)) | Out-Null
$connection.open()
$data_reader = $command.ExecuteReader()
$plain_data = @()
$row_num = 1
[System.Data.DataRow]$data_record = $null
if ($data_table -eq $null) {}
else {

  foreach ($data_record in $data_table) {
    $data_record | Out-Null
    # Reading the columns of the current row

    $row_data = @{
      'id' = $null;
      'baseUrl' = $null;
      'status' = $null;
      'date' = $null;
      'result' = $null;
      'guid' = $null;
      'environment' = $null ;
      'testName' = $null;

    }

    [string[]]($row_data.Keys) | ForEach-Object {
      # An error occurred while enumerating through a collection: Collection was
      # modified; enumeration operation may not execute..
      $cell_name = $_
      $cell_value = $data_record."${cell_name}"
      $row_data[$cell_name] = $cell_value
    }
    Write-Output ("row[{0}]" -f $row_num)
    $row_data
    Write-Output "`n"
    # format needs to be different 
    $plain_data += $row_data
    $row_num++
  }
}

$data_reader.Close()
$command.Dispose()
$connection.Close()

写入结果

function update_single_field {
  param(
    [string]$sql,
    # [ref]$connection does not seem to work here
    # [System.Management.Automation.PSReference]$connection_ref,
    [System.Data.OleDb.OleDbConnection]$connection,
    [string]$where_column_name,
    [object]$where_column_value,
    [string]$update_column_name,
    [object]$update_column_value,
    [System.Management.Automation.PSReference]$update_column_type_ref = ([ref][System.Data.OleDb.OleDbType]::VarChar),
    [System.Management.Automation.PSReference]$where_column_type_ref = ([ref][System.Data.OleDb.OleDbType]::Numeric)
  )

  [System.Data.OleDb.OleDbCommand]$local:command = New-Object System.Data.OleDb.OleDbCommand
  $local:command.Connection = $connection

  $local:command.Parameters.Add($update_column_name,$update_column_type_ref.Value).Value = $update_column_value
  $local:command.Parameters.Add($where_column_name,$where_column_type_ref.Value).Value = $where_column_value
  $local:command.CommandText = $sql

  # TODO: Exception calling "Prepare" with "0" argument(s): "OleDbCommand.Prepare method requires all variable length parameters to have an explicitly set non-zero Size."
  # $command.Prepare()

  $local:result = $local:command.ExecuteNonQuery()
  Write-Output ('Update query: {0}' -f (($sql -replace $update_column_name,$update_column_value) -replace $where_column_name,$where_column_value))
  Write-Output ('Update result: {0}' -f $local:result)

  $local:command.Dispose()

  return $local:result

}

update_single_field `
   -connection $connection `
   -sql "UPDATE [${sheet_name}] SET [status] = @status WHERE [id] = @id" `
   -update_column_name "@status" `
   -update_column_value $false `
   -update_column_type_ref ([ref][System.Data.OleDb.OleDbType]::Boolean) `
   -where_column_name '@id' `
   -where_column_value 2

一些自定义函数被编写。测试框(例如 Spoon.Net)上可能没有安装 Excel,并且当测试数量增加时,选择特定测试重新运行并不方便。网格视图可以有所帮助(可以说是这只是一个初步的解决方案,可能存在更好的解决方案)。

$RESULT_OK = 0
$RESULT_CANCEL = 2
$Readable = @{
  $RESULT_OK = 'OK'
  $RESULT_CANCEL = 'CANCEL'
}


# http://www.cosmonautdreams.com/2013/09/06/Parse-Excel-Quickly-With-Powershell.html
# for singlee column spreadsheets see also
# http://blogs.technet.com/b/heyscriptingguy/archive/2008/09/11/how-can-i-read-from-excel-without-using-excel.aspx

function PromptGrid (
  [System.Collections.IList]$data,
  [object]$caller = $null
) {

  if ($caller -eq $null) {
    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
  }

  [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null
  [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel') | Out-Null
  [System.Reflection.Assembly]::LoadWithPartialName('System.Data') | Out-Null
  [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | Out-Null


  $f = New-Object System.Windows.Forms.Form
  $f.Text = 'Test suite'
  $f.AutoSize = $true
  $grid = New-Object System.Windows.Forms.DataGrid
  $grid.PreferredColumnWidth = 100

  $System_Drawing_Size = New-Object System.Drawing.Size
  $grid.DataBindings.DefaultDataSourceUpdateMode = 0
  $grid.HeaderForeColor = [System.Drawing.Color]::FromArgb(255,0,0,0)

  $grid.Name = 'dataGrid1'
  $grid.DataMember = ''
  $grid.TabIndex = 0
  $System_Drawing_Point = New-Object System.Drawing.Point
  $System_Drawing_Point.X = 13;
  $System_Drawing_Point.Y = 48;
  $grid.Location = $System_Drawing_Point
  $grid.Dock = [System.Windows.Forms.DockStyle]::Fill

  $button = New-Object System.Windows.Forms.Button
  $button.Text = 'Open'
  $button.Dock = [System.Windows.Forms.DockStyle]::Bottom

  $f.Controls.Add($button)
  $f.Controls.Add($grid)
  $button.add_click({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
      # http://msdn.microsoft.com/en-us/library/system.windows.forms.datagridviewrow.cells%28v=vs.110%29.aspx
      # TODO:
      # [System.Windows.Forms.DataGridViewSelectedRowCollection]$rows = $grid.SelectedRows
      # [System.Windows.Forms.DataGridViewRow]$row = $null 
      # [System.Windows.Forms.DataGridViewSelectedCellCollection] $selected_cells = $grid.SelectedCells; 
      $script:Data = 0
      $script:Status = $RESULT_CANCEL
      # $last_row = ($grid.Rows.Count)
      $last_row =  $data.Count
      for ($counter = 0; $counter -lt $last_row;$counter++) { 
        if ($grid.IsSelected($counter)) {
          $row =  $data[$counter]
          $script:Data = $row.Guid
          $script:Status = $RESULT_OK
         }
      }
      $f.Close()

    })

  $grid.DataSource = $data
  $f.ShowDialog() | Out-Null
  $f.Topmost = $True
  $f.Refresh()

}


function display_result {
  param([object[]]$result)
  $script:Data = 0 
  $array = New-Object System.Collections.ArrayList
  foreach ($row_data in $result) {
    $o = New-Object PSObject
    foreach ($row_data_key in $row_data.Keys) {
      $row_data_value = $row_data[$row_data_key]

      $o | Add-Member Noteproperty $row_data_key $row_data_value
    }
    [void]$array.Add($o)
  }

  $process_window = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
  $ret = (PromptGrid $array $process_window)
  if ($script:Status -eq $RESULT_OK ) { 
    Write-Output @( 'Rerun ->', $script:Data )
  }
}

gridview

完整的脚本源代码可在源 zip 文件中找到。

纯 ListView 容器的渲染方式如下:

function PromptListView
{
  param(
    [System.Collections.IList]$data_rows,
    [string[]]$column_names = $null,
    [string[]]$column_tags,
    [bool]$debug
  )
  @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }

  $numCols = $column_names.Count

  # figure out form width
  $width = $numCols * 120

  $title = 'Select process'
  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title
  $f.Size = New-Object System.Drawing.Size ($width,400)
  $f.StartPosition = 'CenterScreen'

  $f.KeyPreview = $true

  $select_button = New-Object System.Windows.Forms.Button
  $select_button.Location = New-Object System.Drawing.Size (10,10)
  $select_button.Size = New-Object System.Drawing.Size (70,23)
  $select_button.Text = 'Select'
  $select_button.add_click({
      # TODO: implementation 
      # select_sailing ($script:Item)
    })

  $button_panel = New-Object System.Windows.Forms.Panel
  $button_panel.Height = 40
  $button_panel.Dock = 'Bottom'
  $button_panel.Controls.AddRange(@( $select_button))


  $panel = New-Object System.Windows.Forms.Panel
  $panel.Dock = 'Fill'
  $f.Controls.Add($panel)
  $list_view = New-Object windows.forms.ListView
  $panel.Controls.AddRange(@( $list_view,$button_panel))


  # create the columns
  $list_view.View = [System.Windows.Forms.View]'Details'
  $list_view.Size = New-Object System.Drawing.Size ($width,350)
  $list_view.FullRowSelect = $true
  $list_view.GridLines = $true
  $list_view.Dock = 'Fill'
  foreach ($col in $column_names) {
    [void]$list_view.Columns.Add($col,100)
  }

  # populate the view
  foreach ($data_row in $data_rows) {
    # NOTE: special processing of first column
    $cell = (Invoke-Expression (('$data_row.{0}' -f $column_names[0]))).ToString()
    $item = New-Object System.Windows.Forms.ListViewItem ($cell)
    for ($i = 1; $i -lt $column_names.Count; $i++) {
      $cell = (Invoke-Expression ('$data_row.{0}' -f $column_names[$i]))

      if ($cell -eq $null) {
        $cell = ''
      }
      [void]$item.SubItems.Add($cell.ToString())
    }
    $item.Tag = $data_row
    [void]$list_view.Items.Add($item)
  }
  <#
  $list_view.add_ItemActivate({
      param(
        [object]$sender,[System.EventArgs]$e)

      [System.Windows.Forms.ListView]$lw = [System.Windows.Forms.ListView]$sender
      [string]$filename = $lw.SelectedItems[0].Tag.ToString()
    })
  #>

  # store the selected item id
  $list_view.add_ItemSelectionChanged({
      param(
        [object]$sender,[System.Windows.Forms.ListViewItemSelectionChangedEventArgs]$e)

      [System.Windows.Forms.ListView]$lw = [System.Windows.Forms.ListView]$sender
      [int]$process_id = 0
      [int32]::TryParse(($e.Item.SubItems[0]).Text,([ref]$process_id))
      $script:Item = $process_id
      # write-host ( '-> {0}' -f $script:Item )
    })

  # tags for sorting
  for ($i = 0; $i -lt $column_tags.Count; $i++) {
    $list_view.Columns[$i].Tag = $column_tags[$i]
  }
  # see below..
  $list_view.Add_ColumnClick({
      $list_view.ListViewItemSorter = New-Object ListViewItemComparer ($_.Column,$script:IsAscending)
      $script:IsAscending = !$script:IsAscending
    })
  $script:Item = 0
  $script:IsAscending = $false
  $f.Topmost = $True
  $script:IsAscending = $false
  $f.Add_Shown({ $f.Activate() })
  $x = $f.ShowDialog()
}

带有排序

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Collections;

public class ListViewItemComparer : System.Collections.IComparer
{
    public int col = 0;
    public System.Windows.Forms.SortOrder Order;
    public ListViewItemComparer()
    {
        col = 0;
    }

    public ListViewItemComparer(int column, bool asc)
    {
        col = column;
        if (asc)
        { Order = SortOrder.Ascending; }
        else
        { Order = SortOrder.Descending; }
    }

    public int Compare(object x, object y)
    {
        if (!(x is ListViewItem)) return (0);
        if (!(y is ListViewItem)) return (0);

        ListViewItem l1 = (ListViewItem)x;
        ListViewItem l2 = (ListViewItem)y;

        if (l1.ListView.Columns[col].Tag == null)
        {
            l1.ListView.Columns[col].Tag = "Text";
        }

        if (l1.ListView.Columns[col].Tag.ToString() == "Numeric")
        {
            float fl1 = float.Parse(l1.SubItems[col].Text);
            float fl2 = float.Parse(l2.SubItems[col].Text);
            return (Order == SortOrder.Ascending) ?  fl1.CompareTo(fl2) :  fl2.CompareTo(fl1);
        }
        else
        {
            string str1 = l1.SubItems[col].Text;
            string str2 = l2.SubItems[col].Text;
            return (Order == SortOrder.Ascending) ? str1.CompareTo(str2) : str2.CompareTo(str1);
        }

    }
}

listview

function display_result {
  param([object[]]$result)
  $column_names = @(
    'id',
    'dest',
    'port',
    'state',
    'title',
    'link'
  )
  $column_tags = @(
    'Numeric',
    'Text',
    'Text',
    'Text',
    'Text',
    'Text'

  )
  $data_rows = New-Object System.Collections.ArrayList
  foreach ($row_data in $result) {
    $o = New-Object PSObject
    foreach ($row_data_key in $column_names) {
      $row_data_value = $row_data[$row_data_key]
      $o | Add-Member Noteproperty $row_data_key $row_data_value
    }
    [void]$data_rows.Add($o)
  }

  [void](PromptListView -data_rows $data_rows -column_names $column_names -column_tags $column_tags)
}

填充 GridView DataTable

一次将数据加载到网格或列表视图中可能不是理想的界面。通用的字典列表似乎不起作用,作为一种解决方法,您可以将其存储在合适的类中。

    public class DictionaryContainer
    {
        private List<Dictionary<string, object>> _data = new List<Dictionary<string, object>> { };

        public List<Dictionary<string, object>> Data
        {
            get { return _data; }
        }

        public void add_row(Dictionary<string, object> row)
        {
            _data.Add(row);
        }

        public DictionaryContainer()
        {
        }
    }

在此示例中,使用了带有切换所有复选框的 DataGridView 类来渲染数据。

function SelectAllGrid {

  param(
    [string]$title,
    [string]$message
  )


  @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title

  $f.Size = New-Object System.Drawing.Size (470,235)
  $f.AutoScaleDimensions = New-Object System.Drawing.SizeF (6.0,13.0)
  $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
  $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
  $f.StartPosition = 'CenterScreen'

  $urls = @( 'http://www.travelocity.com/','http://www.bcdtravel.com/','http://www.airbnb.com','http://www.priceline.com','http://www.tripadvisor.com')


  # https://groups.google.com/forum/#!topic/microsoft.public.windows.powershell/Ta9NyFPovgI 
  $array_of_dictionaries_container = New-Object -Type 'Custom.DictionaryContainer'

  for ($cnt = 0; $cnt -ne 5; $cnt++) {
    $item = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
    $item.Add('RandomNo',(Get-Random -Minimum 1 -Maximum 10001))
    $item.Add('date',(Date))
    $item.Add('url',$urls[$cnt])
    $array_of_dictionaries_container.add_row($item)
  }

  $r = New-Object -TypeName 'Custom.SelectAllGrid' -ArgumentList $array_of_dictionaries_container
  $r.Size = $f.Size

  $f.Controls.Add($r)
  $f.Topmost = $True

  $f.Add_Shown({ $f.Activate() })

  [void]$f.ShowDialog()
  $f.Dispose()

}
$script:Data = $null
SelectAllGrid -Title 'Selection Grid Sample Project'

它被修改为 Panel 而不是 Form,并接受

        private System.Windows.Forms.DataGridView dgvSelectAll;
        public SelectAllGrid(DictionaryContainer userDataContainer = null)
        {
           this.dgvSelectAll = new System.Windows.Forms.DataGridView();
           // ... misc initialization code 
           dgvSelectAll.DataSource = GetDataSource(userDataContainer);
        }
        public DataTable GetDataSource(DictionaryContainer userDataContainer = null)
        {
            DataTable dTable = new DataTable();

            DataRow dRow = null;
            List> sampleData;
            if (userDataContainer == null)
            {
                Random rnd = new Random();
                sampleData = new List> {

                              new Dictionary { { "RandomNo", rnd.NextDouble()}, { "Date", DateTime.Now.ToString("MM/dd/yyyy") }, { "url", "www.facebook.com"}} ,
                              new Dictionary { { "RandomNo", rnd.NextDouble()}, { "Date", DateTime.Now.ToString("MM/dd/yyyy") }, { "url", "www.linkedin.com"}} ,
                              new Dictionary { { "RandomNo", rnd.NextDouble()}, { "Date", DateTime.Now.ToString("MM/dd/yyyy") }, { "url", "www.odesk.com"}}  
                              };
            }
            else
            {
                sampleData = userDataContainer.Data;
            }
            Dictionary openWith = sampleData[0];

            Dictionary.KeyCollection keyColl = openWith.Keys;

            dTable.Columns.Add("IsChecked", System.Type.GetType("System.Boolean"));
            foreach (string s in keyColl)
            {
                dTable.Columns.Add(s);
            }

            foreach (Dictionary objitem in sampleData)
            {
                dRow = dTable.NewRow();
                foreach (KeyValuePair kvp in objitem)
                {
                    dRow[kvp.Key] = kvp.Value.ToString();
                }
                dTable.Rows.Add(dRow);
                dTable.AcceptChanges();

            }
            return dTable;
        }

password

请注意,修改 SelectAllGrid 以直接接受 List<Dictionary<string, object>> 并通过以下方式传递数据

$array_of_dictionaries = New-Object 'System.Collections.Generic.List[System.Collections.Generic.Dictionary[String,Object]]'
for ($cnt = 0; $cnt -ne 5; $cnt++) {
    $item = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
    $item.Add('RandomNo',(Get-Random -Minimum 1 -Maximum 10001))
    $item.Add('date',(Date))
    $item.Add('url',$urls[$cnt])
    $array_of_dictionaries.Add($item)
  }
  $array_of_dictionaries | ForEach-Object { $row = $_
    $row | Format-List
  }
 $r = New-Object -TypeName 'Custom.SelectAllGrid' -ArgumentList $array_of_dictionaries

会导致错误

New-Object : Cannot find an overload for "SelectAllGrid" and the argument count: "5".

并且必须将 System.Data.dll 添加到 Custom.SelectAllGrid 的引用程序集列表中以防止错误。

Add-Type : c:\Documents and Settings\Administrator\Local Settings\Temp\ypffadcb.0.cs(90) : 
The type 'System.Xml.Serialization.IXmlSerializable' is defined in an assembly that is not referenced. 
You must add a reference to assembly 
'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

带可折叠组的列表

下一个示例使用可折叠组控件向用户提供聚合配置信息。

function GroupedListBox
{
  param(
    [string]$title,
 [bool]$show_buttons)


  @('System.Drawing','System.Collections', 'System.Collections.Generic' , 'System.Drawing', 'System.ComponentModel', 'System.Windows.Forms', 'System.Data') |  foreach-object {   [void] [System.Reflection.Assembly]::LoadWithPartialName($_) } 

  $f = New-Object System.Windows.Forms.Form

  $f.Text = $title
  $width = 500
  $f.Size = New-Object System.Drawing.Size ($width,400)
  $glc = New-Object -TypeName 'GroupedListControl.GroupListControl'
  $glc.SuspendLayout()

  $glc.AutoScroll = $true
  $glc.BackColor = [System.Drawing.SystemColors]::Control
  $glc.FlowDirection = [System.Windows.Forms.FlowDirection]::TopDown
  $glc.SingleItemOnlyExpansion = $false
  $glc.WrapContents = $false
  $glc.Anchor = ([System.Windows.Forms.AnchorStyles](0 `
         -bor [System.Windows.Forms.AnchorStyles]::Top `
         -bor [System.Windows.Forms.AnchorStyles]::Bottom `
         -bor [System.Windows.Forms.AnchorStyles]::Left `
         -bor [System.Windows.Forms.AnchorStyles]::Right `
      ))

  $f.SuspendLayout()

  if ($show_buttons) {
        [System.Windows.Forms.CheckBox]$cb1 = new-object -TypeName 'System.Windows.Forms.CheckBox'
        $cb1.AutoSize = $true
        $cb1.Location = new-object System.Drawing.Point(12, 52)
        $cb1.Name = "chkSingleItemOnlyMode"
        $cb1.Size = new-object System.Drawing.Size(224, 17)
        $cb1.Text = 'Single-Group toggle'
        $cb1.UseVisualStyleBackColor = $true
        function chkSingleItemOnlyMode_CheckedChanged
        {
         param([Object] $sender, [EventArgs] $e)
            $glc.SingleItemOnlyExpansion = $cb1.Checked
            if ($glc.SingleItemOnlyExpansion) {
                $glc.CollapseAll()
            } else {
                $glc.ExpandAll()
            }
        }
        $cb1.Add_CheckedChanged({ chkSingleItemOnlyMode_CheckedChanged } )
        [System.Windows.Forms.Label]$label1 = new-object -TypeName 'System.Windows.Forms.Label' 
        $label1.Location = new-object System.Drawing.Point(12, 13)
        $label1.Size = new-object System.Drawing.Size(230, 18) 
	$label1.Text = 'Grouped List Control Demo'
        # $label1.Font = new System.Drawing.Font("Lucida Sans", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)))
        [System.Windows.Forms.Button]$button1 = new-object -TypeName 'System.Windows.Forms.Button' 

            $button1.Location = new-object System.Drawing.Point(303, 46)
            $button1.Name = "button1"
            $button1.Size = new-object System.Drawing.Size(166, 23)
            $button1.TabIndex = 3
            $button1.Text = 'Add Data Items (disconnected)'
            $button1.UseVisualStyleBackColor = true
            $button1.Add_Click( { write-host $glc.GetType() 
$x =  $glc | get-member
write-host ($x -join "`n")
})
         
    $f.Controls.Add($cb1)
    $f.Controls.Add($button1)
    $f.Controls.Add($label1)

    $glc.Location =  new-object System.Drawing.Point(0, 75)
    $glc.Size =  new-object  System.Drawing.Size($f.size.Width, ($f.size.Height - 75))

  } else { 
  $glc.Size = $f.Size

}

  for ($group = 1; $group -le 5; $group++)
  {
    [GroupedListControl.ListGroup]$lg = New-Object -TypeName 'GroupedListControl.ListGroup'
    $lg.Columns.Add("List Group " + $group.ToString(), 120 )
    $lg.Columns.Add("Group " + $group + " SubItem 1", 150 )
    $lg.Columns.Add("Group " + $group + " Subitem 2", 150 )
    $lg.Name = ("Group " + $group)
    # add some sample items:
    for ($j = 1; $j -le 5; $j++){
      [System.Windows.Forms.ListViewItem]$item = $lg.Items.Add(("Item " + $j.ToString()))
      $item.SubItems.Add($item.Text + " SubItem 1")
      $item.SubItems.Add($item.Text + " SubItem 2")
    }

    $glc.Controls.Add($lg)
  }

  $f.Controls.Add($glc)
  $glc.ResumeLayout($false)

  $f.ResumeLayout($false)

  $f.StartPosition = 'CenterScreen'

  $f.KeyPreview = $True

  $f.Topmost = $True

  $caller = New-Object -TypeName 'Win32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

  $f.Add_Shown({ $f.Activate() })

  [void]$f.ShowDialog([win32window]($caller))
  $f.Dispose()
  $result = $caller.Message
  $caller = $null
  return $result
}
$show_buttons_arg  = $false

  if ($PSBoundParameters["show_buttons"]) {
$show_buttons_arg = $true
  }

要传递要显示的数据,请使用以下结构:

$configuration_discovery_results = @{
  'Web.config' = @{
    'COMMENT' = 'Web Server';
    'DOMAIN' = '';
    'CONFIGURATIONS' = @{
      'Exit SSL cms targetted offers' = $Extract_appSetting;
      'Force Non Https for Home Page' = $Extract_appSetting;
      'To new deck plans page' = $Extract_RuleActionurl ;
      'imagesCdnHostToPrepend' = $Extract_RuleActionurl ;
    };
  };

[scriptblock]$Extract_appSetting = {
  param(
    [System.Management.Automation.PSReference]$object_ref,
    [System.Management.Automation.PSReference]$result_ref,
    [string]$key = $null
  )

  if ($key -eq $null -or $key -eq '') {
    throw 'Key cannot be null'

  }

[scriptblock]$Extract_RuleActionurl = {
  param(
    [System.Management.Automation.PSReference]$object_ref,
    [System.Management.Automation.PSReference]$result_ref,
    [string]$key = $null
  )

  if ($key -eq $null -or $key -eq '') {
    throw 'Key cannot be null'
  }

  $data = @{}
  $nodes = $object_ref.Value.Configuration.Location.'system.webServer'.rewrite.rules.rule
  if ($global:debug) {
    Write-Host $nodes.count
  }
  for ($cnt = 0; $cnt -ne $nodes.count; $cnt++) {

    $k = $nodes[$cnt].Getattribute('name')
    $v = $nodes[$cnt].action.Getattribute('url')
    if ($k -match $key) {
      $data[$k] += $v
      if ($global:debug) {
        Write-Output $k; Write-Output $v
      }
    }

  }

  $result_ref.Value = $data[$key]
}

  $data = @{}
  $nodes = $object_ref.Value.Configuration.Location.appSettings.Add
  for ($cnt = 0; $cnt -ne $nodes.count; $cnt++) {
    $k = $nodes[$cnt].Getattribute('key')
    $v = $nodes[$cnt].Getattribute('value')

    if ($k -match $key) {
      if ($global:debug) {
        Write-Host $k
        Write-Host $key
        Write-Host $v
      }
      $data[$k] += $v
    }
  }
  $result_ref.Value = $data[$key]
}

要从各种 *.config 文件收集数据,请使用例如代码:

function collect_config_data {

  param(
    [ValidateNotNull()]
    [string]$target_domain,
    [string]$target_unc_path,
    [scriptblock]$script_block,
    [bool]$verbose,
    [bool]$debug
  )

  $local:result = @()
  if (($target_domain -eq $null) -or ($target_domain -eq '')) {
    if ($powerless) {
      return $local:result
    } else {
      throw 'unspecified DOMAIN'
    }
  }

  [xml]$xml_config = Get-Content -Path $target_unc_path
  $object_ref = ([ref]$xml_config)
  $result_ref = ([ref]$local:result)

  Invoke-Command $script_block -ArgumentList $object_ref,$result_ref,$verbose,$debug

  if ($verbose) {
    Write-Host ("Result:`r`n---`r`n{0}`r`n---`r`n" -f ($local:result -join "`r`n"))
  }

}

要填充列表,请使用:

  foreach ($key in $configuration_discovery_results.Keys) {
    $values = $configuration_discovery_results[$key]
    $configurations = $values['CONFIGURATIONS']
    [GroupedListControl.ListGroup]$lg = New-Object -TypeName 'GroupedListControl.ListGroup'
    $lg.Columns.Add($values['COMMENT'],120)
    $lg.Columns.Add("Key",150)
    $lg.Columns.Add("Value",300)
    # TODO - document the error.
    #    $configurations.Keys | foreach-object {  
    foreach ($k in $configurations.Keys) {
      $v = $configurations[$k]
      [System.Windows.Forms.ListViewItem]$item = $lg.Items.Add($key)
      $item.SubItems.Add($k)
      $item.SubItems.Add($v)
    }

    $glc.Controls.Add($lg)

  }

password

拖放

下一个示例涵盖了拖放列表框。有大量的事件需要处理,并且将 MSDN 示例 http://msdn.microsoft.com/en-us/library/system.windows.forms.control.dodragdrop%28v=vs.100%29.aspx 从 C# 转换为 PowerShell 语法是极其冗余且容易出错的。您只需要最终的 ListDragTarget.Items,因此您可以向 Add-Type 添加一个字符串 getter 方法,让其余部分保持不变,去掉主入口点。

  public class DragNDrop : System.Windows.Forms.Panel
  {

  private string _message;
  public string Message
   {
      get {
                _message = "";
                List<string>  _items = new List<string>();
                foreach (object _item in ListDragTarget.Items) {
                   _items.Add(_item.ToString());
                }
                _message = String.Join(",", _items.ToArray() );
                return _message;
         }
      set { _message = value; }

        private System.Windows.Forms.ListBox ListDragSource;
        private System.Windows.Forms.ListBox ListDragTarget;
        private System.Windows.Forms.CheckBox UseCustomCursorsCheck;
        private System.Windows.Forms.Label DropLocationLabel;

        private int indexOfItemUnderMouseToDrag;
        private int indexOfItemUnderMouseToDrop;

        private Rectangle dragBoxFromMouseDown;
        private Point screenOffset;

        private Cursor MyNoDropCursor;
        private Cursor MyNormalCursor;

        /// The main entry point for the application removed.

        public DragNDrop(String message)
        {

// rest of the code see http://msdn.microsoft.com/en-us/library/system.windows.forms.control.dodragdrop%28v=vs.100%29.aspx

并将构造函数更改为接受 String message。此外,在使 DragNDrop 类继承自 System.Windows.Forms.Panel 而不是 System.Windows.Forms.Form 后,它将被放置在窗体上。

function PromptWithDragDropNish {
param
(

[String] $title,
        [Object] $caller
)

[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')


$f = New-Object System.Windows.Forms.Form
$f.Text = $title

$panel = New-Object DragNDrop($caller.Message)

$f.ClientSize = new-object  System.Drawing.Size(288, 248)
$f.Controls.AddRange(@( $panel ))
$f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog

您可以使用 $caller 对象来处理这里的 Message,同时考虑潜在的附加功能,尽管它不是严格必需的。最后,脚本接收结果。

  $f.Add_Shown( { $f.Activate() } )

  [Void] $f.ShowDialog([Win32Window ] ($caller) )
  $result = $panel.Message
  $panel.Dispose()
  $f.Dispose()

  $caller = $null

  return $result

}

$data = @(
   'one','two','three','four','five',
   'six','seven','nine','ten','eleven'
)
$caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

$caller.Message = $data -join ','
$result = PromptWithDragDropNish 'Items'  $caller

# write-debug ('Selection is : {0}' -f  , $result )

$result -split ',' | format-table -autosize

grid

窗体相应地调整了光标——这在屏幕截图中未显示。窗体关闭后,脚本会打印选定的项目。这样的控件可能很有用,例如安排 Selenium 测试到子集中(转换为和从 *.orderedtests 资源未显示)。完整的脚本源代码可在源 zip 文件中找到。

grid

DF5B1F66EB484A2E8DDC06BD183B0E3F

上下

对于时间间隔选择,可以使用 DateTimePicker 和合适的 System.Windows.Forms.DateTimePickerFormat

或者甚至是 DomainUpDown 派生的自定义时间选择器类

// http://stackoverflow.com/questions/16789399/looking-for-time-picker-control-with-half-hourly-up-down
public class CustomTimePicker : System.Windows.Forms.DomainUpDown
    public CustomTimePicker()
    {         
        // build the list of times...
        for (double time = 23.5; time >= 0; time -= 0.5)
        {
            int hour = (int)time; 
            int minutes = (int)((time - hour) * 60); 
            this.Items.Add(hour.ToString("00") + ":" + minutes.ToString("00")); 
        }
        this.SelectedIndex = Items.IndexOf("09:00"); // select a default time
        this.Wrap = true; 
    }

updown

$form_onload = {
   $script:numeric_value = 0
   $script:time_value = ''
   $script:custom_value= ''
function UpDownsPrompt
  param(
    [object]$caller
  )
  @( 'System.Drawing',
    'System.Collections.Generic',
    'System.Collections',
    'System.ComponentModel',
    'System.Windows.Forms',
    'System.Text',
    'System.Data'
  ) | ForEach-Object { $assembly = $_; [void][System.Reflection.Assembly]::LoadWithPartialName($assembly) }
  $f = New-Object System.Windows.Forms.Form
  $f.Size = New-Object System.Drawing.Size (180,120)
  $n = New-Object System.Windows.Forms.NumericUpDown
  $n.SuspendLayout()
  $n.Parent = $this
  $n.Location = New-Object System.Drawing.Point (30,80)
  $n.Size = New-Object System.Drawing.Size (50,20)
  $n.Value = 1
  $n.Minimum = 0
  $n.Maximum = 1000
  $n.Increment = 1
  $n.DecimalPlaces = 0
  $n.ReadOnly = $false
  $n.TextAlign = [System.Windows.Forms.HorizontalAlignment]::Right
  ($n.add_ValueChanged).Invoke({
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
      $script:numeric_value = $n.Value
    }
  )
  $c = New-Object CustomTimePicker
  $c.Parent = $f
  $c.Location = New-Object System.Drawing.Point (30,50)
  $c.Size = New-Object System.Drawing.Size (70,20)
  $c.TextAlign = [System.Windows.Forms.HorizontalAlignment]::Left
  $c.ReadOnly = $true
  ($c.add_TextChanged).Invoke({
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
        $script:custom_value = $c.SelectedItem.ToString()
    }
  )
  $c.SuspendLayout()
  $c.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
  $c.ReadOnly = $true
  $c.TabIndex = 0
  $c.TabStop = $false
  $s = New-Object System.Windows.Forms.DateTimePicker
  $s.Parent = $f
  $s.Location = New-Object System.Drawing.Point (30,20)
  $s.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
  $s.Size = New-Object System.Drawing.Size (70,20)
  $s.Format = [System.Windows.Forms.DateTimePickerFormat]::Custom
  $s.CustomFormat = 'hh:mm'
  $s.ShowUpDown = $true
  $s.Checked = $false
  $s.Add_VisibleChanged({
      param(
        [object]$sender,
        [System.EventArgs]$eventargs)
      $script:datetime_value = $s.Value
    })
  $f.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
  $f.ClientSize = New-Object System.Drawing.Size (180,120)
  $components = New-Object System.ComponentModel.Container
  $f.Controls.AddRange(@( $c,$n,$s))
  $f.Name = 'Form1'
  $f.Text = 'UpDown Sample'
  $c.ResumeLayout($false)
  $n.ResumeLayout($false)
  $f.ResumeLayout($false)
  $f.StartPosition = 'CenterScreen'
  $f.KeyPreview = $True
  $f.Topmost = $True
  $f.Add_Shown({ $f.Activate() })
  [void]$f.ShowDialog()
  $f.add_Load($form_onload)
  $f.Dispose()
$DebugPreference = 'Continue'
Write-Debug ('Time Selection is : {0}' -f $script:datetime_value )
Write-Debug ('Numeric Value is : {0}' -f $script:numeric_value)
Write-Debug ('Custom contol Value is : {0}' -f $script:custom_value)

results

功能区按钮

您可以改编 C#.NET 中的浮动/滑动/移动菜单 来仅包含功能区滑块控件和定时器,同时将 UserControl1 的定义通过继承 Panel(原始 Form1)而不是 Form 并移除默认构造函数来移至 PowerShell。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Ribbon
{

    public class Panel : System.Windows.Forms.Panel
    {

        private System.Windows.Forms.Panel panel1;
        private System.Windows.Forms.Panel panel2;
        private System.Windows.Forms.Button button2;
        private System.Windows.Forms.Button button1;
        private System.Windows.Forms.Panel panel3;
        private System.Windows.Forms.Timer timer1;
        private System.Windows.Forms.Timer timer2;
        private System.Windows.Forms.UserControl _usrCtrl;
        private System.ComponentModel.IContainer components = null;
  // ...
        public Panel(System.Windows.Forms.UserControl u)
        {
         if (u == null)
            throw new ArgumentNullException("Usercontrol required");
          this._usrCtrl = u; 
          InitializeComponent();
        }

然后在 PowerShell 语义中设计所有按钮和子面板。

function PromptRibbon {

  param(
    [string]$title,
    [string]$message,
    [object]$caller
  )


  @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title

  $f.Size = New-Object System.Drawing.Size (470,135)
  $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
  $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
  $f.StartPosition = 'CenterScreen'

  $u = New-Object System.Windows.Forms.UserControl
  $p1 = New-Object System.Windows.Forms.Panel
  $l1 = New-Object System.Windows.Forms.Label

  $p2 = New-Object System.Windows.Forms.Panel
  $l2 = New-Object System.Windows.Forms.Label

  $b1 = New-Object System.Windows.Forms.Button
  $b2 = New-Object System.Windows.Forms.Button
  $b3 = New-Object System.Windows.Forms.Button
  $b4 = New-Object System.Windows.Forms.Button
  $b5 = New-Object System.Windows.Forms.Button
  $b6 = New-Object System.Windows.Forms.Button
  $b7 = New-Object System.Windows.Forms.Button
  $b8 = New-Object System.Windows.Forms.Button
  $b9 = New-Object System.Windows.Forms.Button
  $b10 = New-Object System.Windows.Forms.Button
  $b11 = New-Object System.Windows.Forms.Button
  $b12 = New-Object System.Windows.Forms.Button
  $b13 = New-Object System.Windows.Forms.Button
  $b14 = New-Object System.Windows.Forms.Button
  $b15 = New-Object System.Windows.Forms.Button
  $b16 = New-Object System.Windows.Forms.Button
  $b17 = New-Object System.Windows.Forms.Button
  $b18 = New-Object System.Windows.Forms.Button
  $b19 = New-Object System.Windows.Forms.Button
  $b20 = New-Object System.Windows.Forms.Button
  $p1.SuspendLayout()
  $p2.SuspendLayout()
  $u.SuspendLayout()

  function button_click {
    param(
      [object]$sender,
      [System.EventArgs]$eventargs
    )
    $who = $sender.Text
    [System.Windows.Forms.MessageBox]::Show(("We are processing {0}.`rThere is no callback defined yet." -f $who))
  }

  $callbacks = @{
    'b1' = [scriptblock]{
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
      $who = $sender.Text
      [System.Windows.Forms.MessageBox]::Show(("We are processing`rcallback function for {0}." -f $who))
    };
    'b3' = [scriptblock]{
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
      $who = $sender.Text
      [System.Windows.Forms.MessageBox]::Show(("We are processing`rcallback function defined for {0}." -f $who))
    };

  }

  #  panels
  $cnt = 0
  @(
    ([ref]$p1),
    ([ref]$p2)
  ) | ForEach-Object {
    $p = $_.Value
    $p.BackColor = [System.Drawing.Color]::Silver
    $p.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
    $p.Dock = [System.Windows.Forms.DockStyle]::Left
    $p.Location = New-Object System.Drawing.Point ((440 * $cnt),0)
    $p.Name = ('panel {0}' -f $cnt)
    $p.Size = New-Object System.Drawing.Size (440,100)
    $p.TabIndex = $cnt
    $cnt++
  }

  # labels
  $cnt = 0
  @(
    ([ref]$l1),
    ([ref]$l2)
  ) | ForEach-Object {
    $l = $_.Value
    $l.BackColor = [System.Drawing.Color]::DarkGray
    $l.Dock = [System.Windows.Forms.DockStyle]::Top
    $l.Location = New-Object System.Drawing.Point (0,0)
    $l.Name = ('label {0}' -f $cnt)
    $l.Size = New-Object System.Drawing.Size (176,23)
    $l.TabIndex = 0
    $l.Text = ('Menu Group  {0}' -f $cnt)
    $l.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
    $cnt++
  }
  # buttons
  $positions = @{
    'b1' = @{ 'x' = 6; 'y' = 27; };
    'b2' = @{ 'x' = 6; 'y' = 64; };
    'b3' = @{ 'x' = 92; 'y' = 27; };
    'b4' = @{ 'x' = 92; 'y' = 64; };
    'b5' = @{ 'x' = 178; 'y' = 27; };
    'b6' = @{ 'x' = 178; 'y' = 64; };
    'b7' = @{ 'x' = 264; 'y' = 27; };
    'b8' = @{ 'x' = 264; 'y' = 64; };
    'b9' = @{ 'x' = 350; 'y' = 27; };
    'b10' = @{ 'x' = 350; 'y' = 64; };
    'b11' = @{ 'x' = 6; 'y' = 27; };
    'b12' = @{ 'x' = 6; 'y' = 64; };
    'b13' = @{ 'x' = 92; 'y' = 27; };
    'b14' = @{ 'x' = 92; 'y' = 64; };
    'b15' = @{ 'x' = 178; 'y' = 27; };
    'b16' = @{ 'x' = 178; 'y' = 64; };
    'b17' = @{ 'x' = 264; 'y' = 27; };
    'b18' = @{ 'x' = 264; 'y' = 64; };
    'b19' = @{ 'x' = 350; 'y' = 27; };
    'b20' = @{ 'x' = 350; 'y' = 64; };

  }
  $cnt = 1

  @(
    ([ref]$b1),
    ([ref]$b2),
    ([ref]$b3),
    ([ref]$b4),
    ([ref]$b5),
    ([ref]$b6),
    ([ref]$b7),
    ([ref]$b8),
    ([ref]$b9),
    ([ref]$b10),
    ([ref]$b11),
    ([ref]$b12),
    ([ref]$b13),
    ([ref]$b14),
    ([ref]$b15),
    ([ref]$b16),
    ([ref]$b17),
    ([ref]$b18),
    ([ref]$b19),
    ([ref]$b20)
  ) | ForEach-Object {
    $b = $_.Value
    $b.Name = ('b{0}' -f $cnt)
    $x = $positions[$b.Name].x
    $y = $positions[$b.Name].y
    Write-Debug ('button{0} x = {1}  y = {2}' -f $cnt,$x,$y)
    $b.Location = New-Object System.Drawing.Point ($x,$y)
    $b.Size = New-Object System.Drawing.Size (80,30)
    $b.TabIndex = 1
    $b.Text = ('Button {0}' -f $cnt)
    $b.UseVisualStyleBackColor = $true
    if ($callbacks[$b.Name]) {
      $b.add_click({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          [scriptblock]$s = $callbacks[$sender.Name]
          $local:result = $null
          Invoke-Command $s -ArgumentList $sender,$eventargs


        })

    } else {
      $b.add_click({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          $caller.Data = $sender.Text
          button_click -Sender $sender -eventargs $eventargs

        })

    }
    $cnt++

  }

  # Panel1 label and buttons

  $p1.Controls.Add($l1)
  $p1.Controls.AddRange(@( $b10,$b9,$b8,$b7,$b6,$b5,$b4,$b3,$b2,$b1))

  # Panel2 label and buttons
  $p2.Controls.AddRange(@( $b20,$b19,$b18,$b17,$b16,$b15,$b14,$b13,$b12,$b11))
  $p2.Controls.Add($l2)

  #  UserControl1
  $u.AutoScaleDimensions = New-Object System.Drawing.SizeF (6,13)
  $u.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
  $u.BackColor = [System.Drawing.Color]::Gainsboro

  $u.Controls.AddRange(@( $p2,$p1))
  $u.Name = 'UserControl1'
  $u.Size = New-Object System.Drawing.Size (948,100)
  $p1.ResumeLayout($false)
  $p2.ResumeLayout($false)
  $u.ResumeLayout($false)

并显示带有功能区按钮的窗体。

ribbon

$caller = New-Object -TypeName 'Win32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

PromptRibbon -Title 'Floating Menu Sample Project' -caller $caller
write-output $caller.Data

当存在按钮的回调时,它会被运行,否则将调用通用的 button_clisk。完整的脚本源代码可在源 zip 文件中找到。

自定义调试消息框

下一个示例显示了 自定义消息框变体,将 C# 代码转换为 PowerShell 语义。

function return_response
{
  param(
    [object]$sender,
    [System.EventArgs]$eventargs
  )

  [string ]$button_text = ([System.Windows.Forms.Button]$sender[0]).Text

  if ($button_text -match '(Yes|No|OK|Cancel|Abort|Retry|Ignore)') {
    $script:Result = $button_text
  }
  $f.Dispose()
}

function add_buttons {
  param([psobject]$param)

  switch ($param) {
    ('None') {
      $button_ok.Width = 80
      $button_ok.Height = 24
      $button_ok.Location = New-Object System.Drawing.Point (391,114)
      $button_ok.Text = 'OK'
      $panel.Controls.Add($button_ok)
      $button_ok.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })

    }
    ('OK') {
      $button_ok.Width = 80
      $button_ok.Height = 24
      $button_ok.Location = New-Object System.Drawing.Point (391,114)
      $button_ok.Text = 'OK'
      $panel.Controls.Add($button_ok)
      $button_ok.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })
    }
    ('YesNo') {
      # add No button
      $button_no.Width = 80
      $button_no.Height = 24
      $button_no.Location = New-Object System.Drawing.Point (391,114)
      $button_no.Text = 'No'
      $panel.Controls.Add($button_no)
      $button_no.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })
      # add Yes button
      $button_yes.Width = 80
      $button_yes.Height = 24
      $button_yes.Location = New-Object System.Drawing.Point (($button_no.Location.X - $button_no.Width - 2),114)
      $button_yes.Text = 'Yes'
      $panel.Controls.Add($button_yes)
      $button_yes.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })
    }
    ('YesNoCancel') {
      # add Cancel button
      $button_cancel.Width = 80
      $button_cancel.Height = 24
      $button_cancel.Location = New-Object System.Drawing.Point (391,114)
      $button_cancel.Text = 'Cancel'
      $panel.Controls.Add($button_cancel)
      $button_cancel.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })
      # add No button
      $button_no.Width = 80
      $button_no.Height = 24
      $button_no.Location = New-Object System.Drawing.Point (($button_cancel.Location.X - $button_cancel.Width - 2),114)
      $button_no.Text = 'No'
      $panel.Controls.Add($button_no)
      $button_no.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })

      # add Yes button
      $button_yes.Width = 80
      $button_yes.Height = 24
      $button_yes.Location = New-Object System.Drawing.Point (($button_no.Location.X - $button_no.Width - 2),114)
      $button_yes.Text = 'Yes'
      $panel.Controls.Add($button_yes)
      $button_yes_Response.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })
    }
    ('RetryCancel') {
      # add Cancel button
      $button_cancel.Width = 80
      $button_cancel.Height = 24
      $button_cancel.Location = New-Object System.Drawing.Point (391,114)
      $button_cancel.Text = 'Cancel'
      $panel.Controls.Add($button_cancel)
      $button_cancel.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })
      # add Retry button
      $button_retry.Width = 80
      $button_retry.Height = 24
      $button_retry.Location = New-Object System.Drawing.Point (($button_cancel.Location.X - $button_cancel.Width - 2),114)
      $button_retry.Text = 'Retry'
      $panel.Controls.Add($button_retry)
      $button_retry.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })

    }
    ('AbortRetryIgnore') {
      # add Ignore button
      $button_ignore.Width = 80
      $button_ignore.Height = 24
      $button_ignore.Location = New-Object System.Drawing.Point (391,114)
      $button_ignore.Text = 'Ignore'
      $panel.Controls.Add($button_ignore)
      $button_ignore.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })
      # add Retry button
      $button_retry.Width = 80
      $button_retry.Height = 24
      $button_retry.Location = New-Object System.Drawing.Point (($button_ignore.Location.X - $button_ignore.Width - 2),114)
      $button_retry.Text = 'Retry'
      $panel.Controls.Add($button_retry)
      $button_retry.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })
      #add Abort button
      $button_abort.Width = 80
      $button_abort.Height = 24
      $button_abort.Location = New-Object System.Drawing.Point (($button_retry.Location.X - $button_retry.Width - 2),114)
      $button_abort.Text = 'Abort'
      $panel.Controls.Add($button_abort)
      $button_abort.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          return_response ($sender,$eventargs)
        })
    }
    default {}
  }
}

function add_icon_bitmap {
  param([psobject]$param)

  switch ($param)
  {
    ('Error') {
      $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Error).ToBitmap()
    }
    ('Information') {
      $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Information).ToBitmap()
    }
    ('Question') {
      $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Question).ToBitmap()
    }
    ('Warning') {
      $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Warning).ToBitmap()
    }
    default {
      $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Information).ToBitmap()
    }
  }
}

function click_handler
{

  param(
    [object]$sender,
    [System.EventArgs]$eventArgs
  )
  if ($button_details.Tag.ToString() -match 'collapse')
  {
    $f.Height = $f.Height + $txtDescription.Height + 6
    $button_details.Tag = 'expand'
    $button_details.Text = 'Hide Details'
    $txtDescription.WordWrap = true
    # txtDescription.Focus();
    # txtDescription.SelectionLength = 0;
  }
  elseif ($button_details.Tag.ToString() -match 'expand')
  {
    $f.Height = $f.Height - $txtDescription.Height - 6
    $button_details.Tag = 'collapse'
    $button_details.Text = 'Show Details'
  }
}

function set_message_text
{
  param(
    [string]$messageText,
    [string]$Title,
    [string]$Description
  )
  $label_message.Text = $messageText
  if (($Description -ne $null) -and ($Description -ne ''))
  {
    $txtDescription.Text = $Description
  }
  else
  {
    $button_details.Visible = $false
  }
  if (($Title -ne $null) -and ($Title -ne ''))
  {
    $f.Text = $Title
  }
  else
  {
    $f.Text = 'Your Message Box'
  }
}

function Show1
{
  param(
    [string]$messageText
  )

  $f = New-Object System.Windows.Forms.Form
  $button_details = New-Object System.Windows.Forms.Button
  $button_ok = New-Object System.Windows.Forms.Button
  $button_yes = New-Object System.Windows.Forms.Button
  $button_no = New-Object System.Windows.Forms.Button
  $button_cancel = New-Object System.Windows.Forms.Button
  $button_abort = New-Object System.Windows.Forms.Button
  $button_retry = New-Object System.Windows.Forms.Button
  $button_ignore = New-Object System.Windows.Forms.Button
  $txtDescription = New-Object System.Windows.Forms.TextBox
  $icon_bitmap = New-Object System.Windows.Forms.PictureBox
  $panel = New-Object System.Windows.Forms.Panel
  $label_message = New-Object System.Windows.Forms.Label
  set_message_text $messageText '' $null
  add_icon_bitmap -param 'Information'
  add_buttons -param 'OK'
  DrawBox
  [void]$f.ShowDialog()
  Write-Host ('$script:Result = ' + $script:Result)
  $script:Result
}

function Show2
{
  param(
    [string]$messageText,
    [string]$messageTitle,
    [string]$description
  )
  $f = New-Object System.Windows.Forms.Form
  $button_details = New-Object System.Windows.Forms.Button
  $button_ok = New-Object System.Windows.Forms.Button
  $button_yes = New-Object System.Windows.Forms.Button
  $button_no = New-Object System.Windows.Forms.Button
  $button_cancel = New-Object System.Windows.Forms.Button
  $button_abort = New-Object System.Windows.Forms.Button
  $button_retry = New-Object System.Windows.Forms.Button
  $button_ignore = New-Object System.Windows.Forms.Button
  $txtDescription = New-Object System.Windows.Forms.TextBox
  $icon_bitmap = New-Object System.Windows.Forms.PictureBox
  $panel = New-Object System.Windows.Forms.Panel
  $label_message = New-Object System.Windows.Forms.Label
  set_message_text $messageText $messageTitle $description
  add_icon_bitmap -param 'Information'
  add_buttons -param 'OK'
  DrawBox
  [void]$f.ShowDialog()
  Write-Host ('$script:Result = ' + $script:Result)
  return $script:Result
}

function Show3
{
  param(
    [string]$messageText,
    [string]$messageTitle,
    [string]$description,
    [object]$IcOn,
    [object]$btn
  )

  $f = New-Object System.Windows.Forms.Form
  $button_details = New-Object System.Windows.Forms.Button
  $button_ok = New-Object System.Windows.Forms.Button
  $button_yes = New-Object System.Windows.Forms.Button
  $button_no = New-Object System.Windows.Forms.Button
  $button_cancel = New-Object System.Windows.Forms.Button
  $button_abort = New-Object System.Windows.Forms.Button
  $button_retry = New-Object System.Windows.Forms.Button
  $button_ignore = New-Object System.Windows.Forms.Button
  $txtDescription = New-Object System.Windows.Forms.TextBox
  $icon_bitmap = New-Object System.Windows.Forms.PictureBox
  $panel = New-Object System.Windows.Forms.Panel
  $label_message = New-Object System.Windows.Forms.Label

  set_message_text $messageText $messageTitle $description
  add_icon_bitmap -param $IcOn
  add_buttons -param $btn
  $script:Result = 'Cancel'

  DrawBox
  [void]$f.ShowDialog()
  $f.Dispose()
  Write-Host ('$script:Result = ' + $script:Result)
  return $script:Result
}

function show_exception
{
  param([System.Exception]$ex)

  $f = New-Object System.Windows.Forms.Form
  $button_details = New-Object System.Windows.Forms.Button
  $button_ok = New-Object System.Windows.Forms.Button
  $button_yes = New-Object System.Windows.Forms.Button
  $button_no = New-Object System.Windows.Forms.Button
  $button_cancel = New-Object System.Windows.Forms.Button
  $button_abort = New-Object System.Windows.Forms.Button
  $button_retry = New-Object System.Windows.Forms.Button
  $button_ignore = New-Object System.Windows.Forms.Button
  $txtDescription = New-Object System.Windows.Forms.TextBox
  $icon_bitmap = New-Object System.Windows.Forms.PictureBox
  $panel = New-Object System.Windows.Forms.Panel
  $label_message = New-Object System.Windows.Forms.Label
  set_message_text -Title 'Exception' -messageText $ex.Message -Description $ex.StackTrace
  add_icon_bitmap -param 'Error'
  add_buttons -param 'YesNo'
  DrawBox
  [void]$f.ShowDialog()
  Write-Host ('$script:Result = ' + $script:Result)
  return $script:Result
}

function DrawBox
{
  $f.Controls.Add($panel)
  $panel.Dock = [System.Windows.Forms.DockStyle]::Fill
  # draw picturebox
  $icon_bitmap.Height = 36
  $icon_bitmap.Width = 40
  $icon_bitmap.Location = New-Object System.Drawing.Point (10,11)
  $panel.Controls.Add($icon_bitmap)
  # add textbox
  $txtDescription.Multiline = $true
  $txtDescription.Height = 183
  $txtDescription.Width = 464
  $txtDescription.Location = New-Object System.Drawing.Point (6,143)
  $txtDescription.BorderStyle = [System.Windows.Forms.BorderStyle]::Fixed3D
  $txtDescription.ScrollBars = [System.Windows.Forms.ScrollBars]::Both
  $txtDescription.ReadOnly = $true
  $panel.Controls.Add($txtDescription)

  # add detail button
  $button_details.Height = 24
  $button_details.Width = 80
  $button_details.Location = New-Object System.Drawing.Point (6,114)
  $button_details.Tag = 'expand'
  $button_details.Text = 'Show Details'
  $panel.Controls.Add($button_details)
  $button_details.add_click.Invoke({
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
      click_handler ($sender,$eventargs)
    })

  $label_message.Location = New-Object System.Drawing.Point (64,22)
  $label_message.AutoSize = $true
  $panel.Controls.Add($label_message)
  $f.Height = 360
  $f.Width = 483

  # set form layout
  $f.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
  $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedSingle
  $f.MaximizeBox = $false
  $f.MinimizeBox = $false
  ## frm.FormClosing += new FormClosingEventHandler(frm_FormClosing)
  $f.BackColor = [System.Drawing.SystemColors]::ButtonFace

  ## origin http://www.iconarchive.com/search?q=ico+files&page=7
  $f.Icon = New-Object System.Drawing.Icon ([System.IO.Path]::Combine((Get-ScriptDirectory),"Martz90-Circle-Files.ico"))
  if ($button_details.Tag.ToString() -match 'expand')
  {
    $f.Height = $f.Height - $txtDescription.Height - 6
    $button_details.Tag = 'collapse'
    $button_details.Text = 'Show Details'
  }
}

结合来自 http://poshcode.org 的纯 PowerShell Assert 函数。

function assert {
  [CmdletBinding()]
  param(
    [Parameter(Position = 0,ParameterSetName = 'Script',Mandatory = $true)]
    [scriptblock]$Script,
    [Parameter(Position = 0,ParameterSetName = 'Condition',Mandatory = $true)]
    [bool]$Condition,
    [Parameter(Position = 1,Mandatory = $true)]
    [string]$message)

  $message = "ASSERT FAILED: $message"
  if ($PSCmdlet.ParameterSetName -eq 'Script') {
    try {
      $ErrorActionPreference = 'STOP'
      $success = & $Script
    } catch {
      $success = $false
      $message = "$message`nEXCEPTION THROWN: $($_.Exception.GetType().FullName)"
    }
  }
  if ($PSCmdlet.ParameterSetName -eq 'Condition') {
    try {
      $ErrorActionPreference = 'STOP'
      $success = $Condition
    } catch {
      $success = $false
      $message = "$message`nEXCEPTION THROWN: $($_.Exception.GetType().FullName)"
    }
  }

  if (!$success) {
    $action = Show3 -messageText $message `
       -messageTitle 'Assert failed' `
       -icon $MSGICON.Error `
       -Btn $MSGBUTTON.RetryCancle `
       -Description ("Try:{0}`r`nScript:{1}`r`nLine:{2}`r`nFunction:{3}" -f $Script,(Get-PSCallStack)[1].ScriptName,(Get-PSCallStack)[1].ScriptLineNumber,(Get-PSCallStack)[1].FunctionName)


    if ($action -ne $MSGRESPONSE.Ignore) {
      throw $message
    }
  }
}

稍作修改以显示异常对话框。

Custom Assert Dialog

以及调用堆栈信息,并可选地继续执行。

function Show3
{
  param(
    [string]$messageText,
    [string]$messageTitle,
    [string]$description,
    [object]$IcOn,
    [object]$btn
  )

  $f = New-Object System.Windows.Forms.Form
  $button_details = New-Object System.Windows.Forms.Button
  $button_ok = New-Object System.Windows.Forms.Button
  $button_yes = New-Object System.Windows.Forms.Button
  $button_no = New-Object System.Windows.Forms.Button
  $button_cancel = New-Object System.Windows.Forms.Button
  $button_abort = New-Object System.Windows.Forms.Button
  $button_retry = New-Object System.Windows.Forms.Button
  $button_ignore = New-Object System.Windows.Forms.Button
  $txtDescription = New-Object System.Windows.Forms.TextBox
  $icon_bitmap = New-Object System.Windows.Forms.PictureBox
  $panel = New-Object System.Windows.Forms.Panel
  $label_message = New-Object System.Windows.Forms.Label

  set_message_text $messageText $messageTitle $description
  add_icon_bitmap -param $IcOn
  add_buttons -param $btn
  $script:Result = 'Cancel'

  DrawBox
  [void]$f.ShowDialog()
  $f.Dispose()
  Write-Host ('$script:Result = ' + $script:Result)
  return $script:Result
}

您可以使用此片段来处理常规异常。

Standard Exception Dialog

或各种按钮组合。完整示例可在源 zip 文件中找到(两个版本:一个保留原始 C# 代码,一个简化版)。

杂项。密码

纯文本

现在,假设任务需要向源代码控制、CI 或其他使用自己的身份验证机制且不接受 NTLM 的远程服务进行身份验证。以下代码有助于提示用户名/密码。它使用了标准的 Windows 窗体实践,即屏蔽密码文本框。

password

function PromptPassword(
    [String] $title,
    [String] $user,
        [Object] $caller
    ){

        [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
        [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')

        $f = New-Object System.Windows.Forms.Form
        $f.MaximizeBox = $false;
        $f.MinimizeBox = $false;
        $f.Text = $title

        $l1 = New-Object System.Windows.Forms.Label
        $l1.Location = New-Object System.Drawing.Size(10,20)
        $l1.Size = New-Object System.Drawing.Size(100,20)
        $l1.Text = 'Username'
        $f.Controls.Add($l1)

        $f.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 10, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);

        $t1 = new-object System.Windows.Forms.TextBox
        $t1.Location = new-object System.Drawing.Point(120, 20)
        $t1.Size = new-object System.Drawing.Size(140, 20)
        $t1.Text = $user;
        $t1.Name = 'txtUser';
        $f.Controls.Add($t1);

        $l2 = New-Object System.Windows.Forms.Label
        $l2.Location = New-Object System.Drawing.Size(10,50)
        $l2.Size = New-Object System.Drawing.Size(100,20)
        $l2.Text = 'Password'
        $f.Controls.Add($l2)

        $t2 = new-object System.Windows.Forms.TextBox
        $t2.Location = new-object System.Drawing.Point(120, 50)
        $t2.Size = new-object System.Drawing.Size(140, 20)
        $t2.Text = ''
        $t2.Name = 'txtPassword'
        $t2.PasswordChar = '*'
        $f.Controls.Add($t2)

        $btnOK = new-object System.Windows.Forms.Button
        $x2 = 20
        $y1 = ($t1.Location.Y + $t1.Size.Height + + $btnOK.Size.Height + 20)
        $btnOK.Location = new-object System.Drawing.Point($x2 , $y1 )
        $btnOK.Text = "OK";
        $btnOK.Name = "btnOK";
        $f.Controls.Add($btnOK);

        $btnCancel = new-object System.Windows.Forms.Button
        $x1 = (($f.Size.Width -  $btnCancel.Size.Width) - 20 )

        $btnCancel.Location = new-object System.Drawing.Point($x1, $y1 );
        $btnCancel.Text = 'Cancel';
        $btnCancel.Name = 'btnCancel';
        $f.Controls.Add($btnCancel);
        $s1 = ($f.Size.Width -  $btnCancel.Size.Width) - 20
        $y2 = ($t1.Location.Y + $t1.Size.Height + $btnOK.Size.Height)

        $f.Size = new-object System.Drawing.Size($f.Size.Width,  (($btnCancel.Location.Y +
                             $btnCancel.Size.Height + 40)))

        $btnCancel.Add_Click({$caller.txtPassword = $null ; $caller.txtUser =  $null ;$f.Close()})
        $btnOK.Add_Click({$caller.Data = $RESULT_OK;$caller.txtPassword  = $t2.Text ; $caller.txtUser = $t1.Text;  $f.Close()})

$f.Controls.Add($l)
$f.Topmost = $true


$caller.Data = $RESULT_CANCEL;
$f.Add_Shown( { $f.Activate() } )
$f.KeyPreview = $True
$f.Add_KeyDown({

    if ($_.KeyCode -eq 'Escape')  { $caller.Data = $RESULT_CANCEL }
    else                              { return }
    $f.Close()
})

[Void] $f.ShowDialog([Win32Window ] ($caller) )

$f.Dispose()
}

在此脚本中,我们将 Userpassword 存储在单独的字段中。

$DebugPreference = 'Continue'

$caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

PromptPassword -title 'Enter credentials'  -user 'admin' -caller $caller
if ($caller.Data -ne $RESULT_CANCEL) {
write-debug ("Result is : {0} / {1}  " -f  $caller.TxtUser , $caller.TxtPassword )
}

Active Directory

请注意,上面的示例无意收集用户的 NTLM 凭据,例如更改新安装的 Windows 服务以使用所需的用户凭据执行。在这种情况下,请使用 Microsoft Get-Credential cmdlet。

$DebugPreference = 'Continue'

$target_service_name = 'MsDepSvc'
$domain = $env:USERDOMAIN
if ($domain -like 'UAT') {
  $user = '_uatmsdeploy'
}
elseif ($domain -like 'PROD') {
  $user = '_msdeploy'
}
else {
  $user = $env:USERNAME
}

$target_account = "${domain}\${user}"
$credential = Get-Credential -username $target_account -message 'Please authenticate'

if ($credential -ne $null) {
  $target_account  = $credential.Username
  $target_password  = $credential.GetNetworkCredential().Password
  write-Debug $target_password
} else {

}
return

用于凭据验证、管理员权限、修改新安装的服务的代码已从显示中省略。

password

会话 Cookie

另一种可能的登录场景是当用户可以使用其域凭据进行身份验证,但系统在浏览器内部使用会话 cookie。

您可以创建一个带有 WebBrowser 的对话框,并监视用户何时成功登录,然后收集全局会话 cookie。

为此,将 wininet.dll p/invoke 代码添加到 $caller 对象并在适当的时候调用。处理浏览器 cookie 的解释可在各种来源中找到,例如此处

Add-Type -TypeDefinition @"
// ... c sharp code
"@ -ReferencedAssemblies 'System.Windows.Forms.dll', 'System.Runtime.InteropServices.dll', 'System.Net.dll'

使用代码

using System;
using System.Text;
using System.Net;
using System.Windows.Forms;

using System.Runtime.InteropServices;

public class Win32Window : IWin32Window
{
    private IntPtr _hWnd;
    private string _cookies;
    private string _url;

    public string Cookies
    {
        get { return _cookies; }
        set { _cookies = value; }
    }

    public string Url
    {
        get { return _url; }
        set { _url = value; }
    }

    public Win32Window(IntPtr handle)
    {
        _hWnd = handle;
    }

    public IntPtr Handle
    {
        get { return _hWnd; }
    }

    [DllImport("wininet.dll", SetLastError = true)]
    public static extern bool InternetGetCookieEx(
        string url,
        string cookieName,
        StringBuilder cookieData,
        ref int size,
        Int32 dwFlags,
        IntPtr lpReserved);

    private const int INTERNET_COOKIE_HTTPONLY = 0x00002000;
    private const int INTERNET_OPTION_END_BROWSER_SESSION = 42;

public string GetGlobalCookies(string uri)
    {
        int datasize = 1024;
        StringBuilder cookieData = new StringBuilder((int)datasize);
        if (InternetGetCookieEx(uri, null, cookieData, ref datasize, INTERNET_COOKIE_HTTPONLY, IntPtr.Zero)
            && cookieData.Length > 0)
        {
            return cookieData.ToString().Replace(';', ',');
        }
        else
        {
            return null;
        }
    }

}

没有什么可以阻止一个人使用 Add-Type 存储任意有效的 C# 代码。

password

并在 $browser 对象中处理 Navigated 事件。

function promptForContinueWithCookies(
    [String] $login_url = $null,
    [Object] $caller= $null
    )
{

$f = New-Object System.Windows.Forms.Form
$f.Text = $title

$timer1 = new-object System.Timers.Timer
$label1 = new-object System.Windows.Forms.Label

$f.SuspendLayout()
$components = new-object System.ComponentModel.Container


        $browser = new-object System.Windows.Forms.WebBrowser
        $f.SuspendLayout();

        # webBrowser1
        $browser.Dock = [System.Windows.Forms.DockStyle]::Fill
        $browser.Location = new-object System.Drawing.Point(0, 0)
        $browser.Name = "webBrowser1"
        $browser.Size = new-object System.Drawing.Size(600, 600)
        $browser.TabIndex = 0
        # Form1
        $f.AutoScaleDimensions = new-object System.Drawing.SizeF(6, 13)
        $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
        $f.ClientSize = new-object System.Drawing.Size(600, 600)
        $f.Controls.Add($browser)
        $f.Text = "Login to octopus"
        $f.ResumeLayout($false)

$f.Add_Load({
  param ([Object] $sender, [System.EventArgs] $eventArgs )
$browser.Navigate($login_url);
})

$browser.Add_Navigated(
{

  param ([Object] $sender, [System.Windows.Forms.WebBrowserNavigatedEventArgs] $eventArgs )
        # wait for the user to successfully log in
        # then capture the global cookies and sent to $caller
        $url = $browser.Url.ToString()
        if ($caller -ne $null -and $url -ne $null -and $url -match $caller.Url ) {
            $caller.Cookies = $caller.GetGlobalCookies($url)
        }
    }
 )

$f.ResumeLayout($false)
$f.Topmost = $True

$f.Add_Shown( { $f.Activate() } )

[void] $f.ShowDialog([Win32Window ] ($caller) )

}

password

$caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
$service_host = 'https://:8088'
$login_route = 'app#/users/sign-in'
$login_url = ('{0}/{1}' -f $service_host , $login_route)

$caller.Url =  'app#/environments'

promptForContinueWithCookies $login_url $caller

write-host ("{0}->{1}" -f , $caller.Url, $caller.Cookies)

Cookie 将如下所示:

OctopusIdentificationToken = 6pivzR9B%2fEOyJwbBkA2XfYe1BW4BNuXUqCtpW7VX943Em%2fkBZataiWxOVRDnsiBz

通用对话框

通用对话框是一个很好的候选对象,可以成为 PowerShell 模块(进行中)。

@( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }

function TextInputBox {
  param(
    $prompt_message = 'Enter the Value',
    $caption = 'Inputbox Test'
  )
  $script:result = @{ 'text' = ''; 'status' = $null; }
  $form = New-Object System.Windows.Forms.Form
  $label_prompt = New-Object System.Windows.Forms.Label
  $button_ok = New-Object System.Windows.Forms.Button
  $button_cancel = New-Object System.Windows.Forms.Button
  $text_input = New-Object System.Windows.Forms.TextBox
  $form.SuspendLayout()
  $label_prompt.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
  $label_prompt.BackColor = [System.Drawing.SystemColors]::Control
  $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
  $label_prompt.Location = New-Object System.Drawing.Point (12,9)
  $label_prompt.Name = 'lblPrompt'
  $label_prompt.Size = New-Object System.Drawing.Size (302,82)
  $label_prompt.TabIndex = 3
  $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $button_ok.DialogResult = [System.Windows.Forms.DialogResult]::OK
  $button_ok.FlatStyle = [System.Windows.Forms.FlatStyle]::Standard
  $button_ok.Location = New-Object System.Drawing.Point (326,8)
  $button_ok.Name = 'button_ok'
  $button_ok.Size = New-Object System.Drawing.Size (64,24)
  $button_ok.TabIndex = 1
  $button_ok.Text = '&OK'
  $button_ok.Add_Click({
      param([object]$sender,[System.EventArgs]$e)
      $script:result.status = [System.Windows.Forms.DialogResult]::OK
      $script:result.Text = $text_input.Text
      $form.Dispose()
    })
  $button_ok.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $button_cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
  $button_cancel.FlatStyle = [System.Windows.Forms.FlatStyle]::Standard
  $button_cancel.Location = New-Object System.Drawing.Point (326,40)
  $button_cancel.Name = 'button_cancel'
  $button_cancel.Size = New-Object System.Drawing.Size (64,24)
  $button_cancel.TabIndex = 2
  $button_cancel.Text = '&Cancel'
  $button_cancel.Add_Click({
      param([object]$sender,[System.EventArgs]$e)
      $script:result.status = [System.Windows.Forms.DialogResult]::Cancel
      $text_input.Text = ''
      $script:result.Text = ''
      $form.Dispose()
    })

  $button_cancel.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $text_input.Location = New-Object System.Drawing.Point (8,100)
  $text_input.Name = 'text_input'
  $text_input.Size = New-Object System.Drawing.Size (379,20)
  $text_input.TabIndex = 0
  $text_input.Text = ''
  $text_input.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
  $form.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
  $form.ClientSize = New-Object System.Drawing.Size (398,128)
  $form.Controls.AddRange(@($text_input,$button_cancel,$button_ok,$label_prompt))
  $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
  $form.MaximizeBox = $false
  $form.MinimizeBox = $false
  $form.Name = 'InputBoxDialog'
  $form.ResumeLayout($false)
  $form.AcceptButton = $button_ok
  $form.ShowInTaskbar = $false

  $response = [System.Windows.Forms.DialogResult]::Ignore
  $result = ''
  $text_input.Text = ''
  $label_prompt.Text = $prompt_message
  $form.Text = $caption
  $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen

  $text_input.SelectionStart = 0;
  $text_input.SelectionLength = $text_input.Text.Length
  $text_input.Focus()

  $form.Name = 'Form1'
  $form.ResumeLayout($false)

  $form.Topmost = $Trues

  $form.Add_Shown({ $form.Activate() })

  [void]$form.ShowDialog()

  $form.Dispose()
  $form = $null
  return $script:result
}

combo

function ComboInputBox {

  param(
    [string]$prompt_message = 'Select or Enter the Country',
    [string[]]$items = @(),
    [string]$caption = 'combo test'
  )

function PopulateCombo ()
{
  param([string[]]$comboBoxItems)
  for ($i = 0; $i -lt $comboBoxItems.Length; $i++)
  {
    $str = $comboBoxItems[$i]
    if ($str -ne $null)
    {
      [void]$combobox.Items.Add($str)
    }
  }
}

  $script:result = @{ 'text' = ''; 'status' = $null; }
  $script:result.status = [System.Windows.Forms.DialogResult]::None;

  $form = New-Object System.Windows.Forms.Form
  $label_prompt = New-Object System.Windows.Forms.Label
  $button_ok = New-Object System.Windows.Forms.Button
  $button_cancel = New-Object System.Windows.Forms.Button
  $combobox = New-Object System.Windows.Forms.ComboBox
  $form.SuspendLayout()
  $label_prompt.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
  $label_prompt.BackColor = [System.Drawing.SystemColors]::Control
  $label_prompt.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',8.25,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
  $label_prompt.Location = New-Object System.Drawing.Point (12,9)
  $label_prompt.Name = 'lblPrompt'
  $label_prompt.Size = New-Object System.Drawing.Size (302,82)
  $label_prompt.TabIndex = 3
  $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $button_ok.DialogResult = [System.Windows.Forms.DialogResult]::OK
  $button_ok.FlatStyle = [System.Windows.Forms.FlatStyle]::Standard
  $button_ok.Location = New-Object System.Drawing.Point (326,8)
  $button_ok.Name = 'btnOK'
  $button_ok.Size = New-Object System.Drawing.Size (64,24)
  $button_ok.TabIndex = 1
  $button_ok.Text = '&OK'
  $button_ok.Add_Click({
      param([object]$sender,[System.EventArgs]$e)
      $script:result.status = [System.Windows.Forms.DialogResult]::OK
      $script:result.Text = $combobox.Text
      $form.Dispose()

    })
  $button_ok.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $button_cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
  $button_cancel.FlatStyle = [System.Windows.Forms.FlatStyle]::Standard
  $button_cancel.Location = New-Object System.Drawing.Point (326,40)
  $button_cancel.Name = 'btnCancel'
  $button_cancel.Size = New-Object System.Drawing.Size (64,24)
  $button_cancel.TabIndex = 2
  $button_cancel.Text = '&Cancel'
  $button_cancel.Add_Click({
      param([object]$sender,[System.EventArgs]$e)
      $script:result.status = [System.Windows.Forms.DialogResult]::Cancel
      $script:result.Text = ''
      $form.Dispose()

    })
  $button_cancel.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $combobox.Location = New-Object System.Drawing.Point (8,100)
  $combobox.Name = 'CmBxComboBox'
  $combobox.Size = New-Object System.Drawing.Size (379,20)
  $combobox.TabIndex = 0
  $combobox.Text = ''
  $combobox.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
  $combobox.Add_TextChanged({
      param([object]$sender,[System.EventArgs]$e)

    })

  $combobox.Add_KeyPress({
      param(
        [object]$sender,[System.Windows.Forms.KeyPressEventArgs]$e
      )

    })
  $combobox.Add_TextChanged({
      param(
        [object]$sender,[System.EventArgs]$e
      )

    })


  $form.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
  $form.ClientSize = New-Object System.Drawing.Size (398,128)
  $form.Controls.AddRange(@($combobox,$button_cancel,$button_ok,$label_prompt))
  $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
  $form.MaximizeBox = $false
  $form.MinimizeBox = $false
  $form.Name = 'ComboBoxDialog'
  $form.ResumeLayout($false)
  $form.AcceptButton = $button_ok
  $script:result.status = [System.Windows.Forms.DialogResult]::Ignore
  $script:result.status = ''
  PopulateCombo -comboBoxItems $items
  $label_prompt.Text = $prompt_message
  $form.Text = $caption
  $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
  $combobox.SelectionStart = 0
  $combobox.SelectionLength = $combobox.Text.Length
  $combobox.Focus()
  $form.Name = 'Form1'
  $form.ResumeLayout($false)

  $form.Topmost = $True

  $form.Add_Shown({ $form.Activate() })

  [void]$form.ShowDialog($caller)

  $form.Dispose()
  $form = $null
  return $script:result

}

password2

function ChangePasswordDialogBox {

  param(
    [string]$prompt_message = 'Change the password',
    [string]$caption = 'Default Caption',
    [string]$old_password = 'password'

  )
  $script:result = @{ 'text' = ''; 'status' = $null; }

  $form = New-Object System.Windows.Forms.Form
  $label_old_password = New-Object System.Windows.Forms.Label
  $label_new_password = New-Object System.Windows.Forms.Label
  $label_prompt = New-Object System.Windows.Forms.Label
  $label_confirm_password = New-Object System.Windows.Forms.Label
  $button_ok = New-Object System.Windows.Forms.Button
  $button_cancel = New-Object System.Windows.Forms.Button
  $text_old_password = New-Object System.Windows.Forms.TextBox
  $text_new_password = New-Object System.Windows.Forms.TextBox
  $text_confirm_password = New-Object System.Windows.Forms.TextBox
  $form.SuspendLayout()
  $label_old_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $label_old_password.Location = New-Object System.Drawing.Point (16,88)
  $label_old_password.Name = 'lblOldPassword'
  $label_old_password.Size = New-Object System.Drawing.Size (168,24)
  $label_old_password.TabIndex = 1
  $label_old_password.Text = 'Old Password'
  $label_old_password.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
  $label_new_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $label_new_password.Location = New-Object System.Drawing.Point (16,112)
  $label_new_password.Name = 'lblNewPassword'
  $label_new_password.Size = New-Object System.Drawing.Size (168,24)
  $label_new_password.TabIndex = 2
  $label_new_password.Text = 'New Password'
  $label_new_password.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
  $label_confirm_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $label_confirm_password.Location = New-Object System.Drawing.Point (16,136)
  $label_confirm_password.Name = 'lblConfirmPassword'
  $label_confirm_password.Size = New-Object System.Drawing.Size (168,24)
  $label_confirm_password.TabIndex = 3
  $label_confirm_password.Text = 'Confirm New Password';
  $label_confirm_password.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
  $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
  $label_prompt.Location = New-Object System.Drawing.Point (16,8)
  $label_prompt.Name = 'lblPrompt'
  $label_prompt.Size = New-Object System.Drawing.Size (280,72)
  $label_prompt.TabIndex = 9
  $label_prompt.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
  $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $text_old_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
  $text_old_password.Location = New-Object System.Drawing.Point (192,88)
  $text_old_password.Name = 'txtbxOldPassword'
  $text_old_password.Size = New-Object System.Drawing.Size (184,21);
  $text_old_password.TabIndex = 4
  $text_old_password.Text = ''
  $text_old_password.PasswordChar = '*'
  $text_new_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0);
  $text_new_password.Location = New-Object System.Drawing.Point (192,112)
  $text_new_password.Name = 'txtbxNewPassword'
  $text_new_password.Size = New-Object System.Drawing.Size (184,21)
  $text_new_password.TabIndex = 5
  $text_new_password.Text = ''
  $text_new_password.PasswordChar = '*'
  $text_confirm_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
  $text_confirm_password.Location = New-Object System.Drawing.Point (192,136)
  $text_confirm_password.Name = 'txtbxConfirmPassword'
  $text_confirm_password.Size = New-Object System.Drawing.Size (184,21)
  $text_confirm_password.TabIndex = 6
  $text_confirm_password.Text = ''
  $text_confirm_password.PasswordChar = '*'
  $button_ok.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $button_ok.Location = New-Object System.Drawing.Point (312,16)
  $button_ok.Name = 'button_ok'
  $button_ok.Size = New-Object System.Drawing.Size (64,24)
  $button_ok.TabIndex = 7
  $button_ok.Text = 'OK'
  $button_ok.Add_Click({
      param([object]$sender,[System.EventArgs]$e)
      if ($text_old_password.Text.Trim() -ne $old_password) {
        # MessageBox.Show(ChangePasswordDialogBox.frmInputDialog, 'Incorrect Old Password', 'LinkSys', MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
        $text_old_password.SelectionStart = 0
        $text_old_password.SelectionLength = $text_old_password.Text.Length
        $text_old_password.Focus()
      } else {

        if ($text_new_password.Text.Trim() -ne $text_confirm_password.Text.Trim()) {

          $text_confirm_password.SelectionStart = 0
          $text_confirm_passwordSelectionLength = $text_confirm_password.Text.Length
          $text_confirm_password.Focus()

        } else {

          $script:result.status = [System.Windows.Forms.DialogResult]::OK
          $script:result.Text = $text_new_password.Text
          $form.Dispose()
        } }


    })
  $button_cancel.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
  $button_cancel.Location = New-Object System.Drawing.Point (312,48)
  $button_cancel.Name = 'btnCancel'
  $button_cancel.Size = New-Object System.Drawing.Size (64,24)
  $button_cancel.TabIndex = 8
  $button_cancel.Text = 'Cancel'
  $button_cancel.Add_Click({
      param([object]$sender,[System.EventArgs]$e)
      $script:result.status = [System.Windows.Forms.DialogResult]::Cancel
      $text_input.Text = ''
      $script:result.Text = ''
      $form.Dispose()

    }
  )
  $form.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
  $form.ClientSize = New-Object System.Drawing.Size (400,182)
  $form.Controls.AddRange(@($text_old_password,
$text_new_password,
$text_confirm_password,
$button_cancel,
$button_ok,
$label_prompt,
$label_old_password,
$label_new_password,
$label_confirm_password))
  $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
  $form.MaximizeBox = $false
  $form.MinimizeBox = $false
  $form.Name = 'InputBoxDialog'
  $form.ResumeLayout($false)
  $form.AcceptButton = $button_ok
  $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
  $form.ShowInTaskbar = $false

  $script:result.status = [System.Windows.Forms.DialogResult]::Ignore

  $label_prompt.Text = $prompt_message
  $label_old_password.Text = 'Old Password'
  $label_new_password.Text = 'New Password'
  $label_confirm_password.Text = 'Confirm New Password'
  $text_old_password.Text = $old_password # ''
  $text_new_password.Text = ''
  $text_confirm_password.Text = ''
  $form.Text = $caption
  # Rectangle workingArea = Screen.PrimaryScreen.WorkingArea;

  $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
  $text_old_password.Focus()

  $form.Name = 'Form1'
  $form.ResumeLayout($false)

  $form.Topmost = $Trues

  $form.Add_Shown({ $form.Activate() })

  [void]$form.ShowDialog()

  $form.Dispose()
  $form = $null
  return $script:result
}

chooser

@( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
$shared_assemblies = @(
  'nunit.framework.dll'
)
$shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'
if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
  $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
}
pushd $shared_assemblies_path
$shared_assemblies | ForEach-Object {
  if ($host.Version.Major -gt 2) {
    Unblock-File -Path $_;
  }
  Write-Debug $_
  Add-Type -Path $_
}
popd
$caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
$f = New-Object -TypeName 'System.Windows.Forms.Form'
$f.Text = $title
$f.SuspendLayout()

$f.AutoScaleDimensions = New-Object System.Drawing.SizeF (6.0,13.0)
$f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
$f.ClientSize = New-Object System.Drawing.Size (210,105)
$button_combobox_test = New-Object System.Windows.Forms.Button
$button_combobox_test.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
$button_combobox_test.Location = New-Object System.Drawing.Point (10,10)
$button_combobox_test.Size = New-Object System.Drawing.Size (135,23)
$button_combobox_test.Text = 'Combobox Test'
$button_combobox_test.Add_Click({
    $countries = @(
      "India",
      "USA",
      "UK",
      "Russia",
      "Bulgaria",
      "Singapore",
      "Malayasia",
      "Japan",
      "Thailand"
    )
    $prompt_message = 'Select or Enter the Country'
    $caption = 'Combobox Test' 
    $o = ComboInputBox -items $countries -caption  $caption -prompt_message  $prompt_message 
    if ($o.status -match 'OK') {
      $caller.Data = $o.Text
    $f.Close()
    }
  })
$f.Controls.Add($button_combobox_test)
$button_change_password_test = New-Object System.Windows.Forms.Button
$button_change_password_test.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
$button_change_password_test.Location = New-Object System.Drawing.Point (10,40)
$button_change_password_test.Size = New-Object System.Drawing.Size (135,23)
$button_change_password_test.Text = 'Change Password Test'
$button_change_password_test.Add_Click({
    $prompt_message = 'Change the Password'
    $caption = 'Change Password Test'
    $old_password = '123'
    $o = ChangePasswordDialogBox -prompt_message $prompt_message -caption $caption -old_password $old_password
    if ($o.status -match 'OK') {
      $caller.Data = $o.Text
    $f.Close()
    }

  })
$f.Controls.Add($button_change_password_test)
$button_inputbox_test = New-Object System.Windows.Forms.Button
$button_inputbox_test.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
$button_inputbox_test.Location = New-Object System.Drawing.Point (10,70)
$button_inputbox_test.Size = New-Object System.Drawing.Size (135,23)
$button_inputbox_test.Text = 'Inputbox test'
$button_inputbox_test.Add_Click({
    $prompt_message = 'Enter the Value'
    $caption = 'Inputbox test'
    $o = TextInputBox -caption $caption -prompt_message $prompt_message
    if ($o.status -match 'OK') {
      $caller.Data = $o.Text
    $f.Close()
    }
  })
$f.Controls.Add($button_inputbox_test)
$f.Name = "Form1"
$f.Text = 'Standard Input Dialogs'
$f.ResumeLayout($false)
$f.Topmost = $Trues
$f.Add_Shown({ $f.Activate() })
[void]$f.ShowDialog($caller)
$f.Dispose()
Write-Output $caller.Data

完整示例可在源 zip 文件中找到。

带输入焦点控制的制表对话框

下一个重要主题是制表对话框。实现此类代码基本上与前面所示的相同,增加了一个额外功能——它会阻止用户离开 textbox ,直到有输入为止。在窗体绘制时,会选择特定的选项卡和输入。

如果用户尝试在未填写某些文本的情况下切换到其他选项卡或输入,则会在 TextBox 下显示警告消息。

password

提供输入后,警告消息会被清除。

password

负责此功能的代码突出显示如下:

function PromptWithTabs(
[String] $title,
        [Object] $caller
){
        [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
        [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')

        $f = New-Object System.Windows.Forms.Form
        $f.Text = $title

        $panel2 = new-object System.Windows.Forms.TabPage
        $textbox1 = new-object System.Windows.Forms.TextBox
        $panel1 = new-object System.Windows.Forms.TabPage
        $button1 = new-object System.Windows.Forms.Button
        $tab_contol1 = new-object System.Windows.Forms.TabControl
        $panel2.SuspendLayout()
        $panel1.SuspendLayout()
        $tab_contol1.SuspendLayout()
        $f.SuspendLayout()

        $panel2.Controls.Add($textbox1)
        $panel2.Location = new-object System.Drawing.Point(4, 22)
        $panel2.Name = "tabPage2"
        $panel2.Padding = new-object System.Windows.Forms.Padding(3)
        $panel2.Size = new-object System.Drawing.Size(259, 52)
        $panel2.TabIndex = 1
        $panel2.Text = "Input Tab"

        $textbox1.Location = new-object System.Drawing.Point(72, 7)
        $textbox1.Name = "textBoxMessage"
        $textbox1.Size = new-object System.Drawing.Size(100, 20)
        $textbox1.TabIndex = 0

        $l1 = New-Object System.Windows.Forms.Label
        $l1.Location = New-Object System.Drawing.Size(72,32)
        $l1.Size = New-Object System.Drawing.Size(100,16)
        $l1.Text = ''

        $l1.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 8, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);
        $panel2.Controls.Add($l1)

        $textbox1.Add_Leave( {
           param(
            [Object] $sender,
            [System.EventArgs] $eventargs
            )
            if ($sender.Text.length -eq 0) {
              $l1.Text = 'Input required'
              # [System.Windows.Forms.MessageBox]::Show('Input required')
              $tab_contol1.SelectedIndex = 1
              $sender.Select()
              $result = $sender.Focus()
            } else {
              $l1.Text = ''
            }
         })
        $panel1.Controls.Add($button1)
        $panel1.Location = new-object System.Drawing.Point(4, 22)
        $panel1.Name = "tabPage1"
        $panel1.Padding = new-object System.Windows.Forms.Padding(3)
        $panel1.Size = new-object System.Drawing.Size(259, 52)
        $panel1.TabIndex = 0
        $panel1.Text = "Action Tab"

        $button1.Location = new-object System.Drawing.Point(74, 7)
        $button1.Name = "buttonShowMessage"
        $button1.Size = new-object System.Drawing.Size(107, 24)
        $button1.TabIndex = 0
        $button1.Text = "Show Message"
        $button1_Click = {
           param(
            [Object] $sender,
            [System.EventArgs] $eventargs
            )
            $caller.Message = $textbox1.Text
            [System.Windows.Forms.MessageBox]::Show($textbox1.Text);
        }
        $button1.Add_Click($button1_Click)

        $tab_contol1.Controls.Add($panel1)
        $tab_contol1.Controls.Add($panel2)
        $tab_contol1.Location = new-object System.Drawing.Point(13, 13)
        $tab_contol1.Name = "tabControl1"
        $tab_contol1.SelectedIndex = 1
        $textbox1.Select()
        $textbox1.Enabled = $true
        $tab_contol1.Size = new-object System.Drawing.Size(267, 88)
        $tab_contol1.TabIndex = 0

        $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
        $f.ClientSize = new-object System.Drawing.Size(292, 108)
        $f.Controls.Add($tab_contol1)
        $panel2.ResumeLayout($false)
        $panel2.PerformLayout()
        $panel1.ResumeLayout($false)
        $tab_contol1.ResumeLayout($false)
        $f.ResumeLayout($false)
        $f.ActiveControl = $textbox1

        $f.Topmost = $true


        $f.Add_Shown( { $f.Activate() } )
        $f.KeyPreview = $True


        [Void] $f.ShowDialog([Win32Window ] ($caller) )

        $f.Dispose()
}

注意:操作顺序在上面的片段中很重要。focus()select() 之间存在细微差别,此处未涵盖。

password

单击按钮会启动一个 messagebox ,并将结果存储在 $caller.Message 中。

进度条

下一个示例使用基于 Windows Forms 的自定义 ProgressBar Host 来显示,例如,PowerShell 作业在远程主机上执行某些转储任务的状态。

定义控件类的源代码已导入到脚本中。

Add-Type -TypeDefinition @"

// "

namespace ProgressBarHost
{
    public class Progress : System.Windows.Forms.UserControl
    {
    // code
    }
}
"@ -ReferencedAssemblies 'System.Windows.Forms.dll', 'System.Drawing.dll', 'System.Data.dll', 'System.ComponentModel.dll'

在此示例中,PerformStep 方法将不经修改地使用,但它很可能以特定于领域的方式进行自定义。

PowerShell 脚本执行窗体设计器通常会做的事情。

$so = [hashtable]::Synchronized(@{
    'Progress'  = [ProgressBarHost.Progress] $null ;
    })

$rs =[runspacefactory]::CreateRunspace()
$rs.ApartmentState = 'STA'
$rs.ThreadOptions = 'ReuseThread'
$rs.Open()
$rs.SessionStateProxy.SetVariable('so', $so)


$run_script = [PowerShell]::Create().AddScript({

function Progressbar(
    [String] $title,
    [String] $message
    ){

[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')


$f = New-Object System.Windows.Forms.Form
$f.Text = $title

$f.Size = New-Object System.Drawing.Size(650,120)
$f.StartPosition = 'CenterScreen'

$p = new-object ProgressBarHost.Progress
$p.Location = new-object System.Drawing.Point(12, 8)
$p.Name = 'status'
$p.Size = new-object System.Drawing.Size(272, 88)
$p.TabIndex = 0
$so.Progress = $p

$b = New-Object System.Windows.Forms.Button
$b.Location = New-Object System.Drawing.Size(140, 152)
$b.Size = New-Object System.Drawing.Size(92, 24)
$b.Text = 'forward'
$b.Add_Click({ $p.PerformStep()
               if ($p.Maximum -eq $p.Value) {
                   $b.Enabled = false;
              }
         })

$f.Controls.Add($b)
$f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 14)
$f.ClientSize = new-object System.Drawing.Size(292, 194)
$f.Controls.Add($p )
$f.Topmost = $True
$f.Add_Shown( { $f.Activate() } )

[Void] $f.ShowDialog( )

$f.Dispose()
}

Progressbar -title $title -message $message

})

# -- main program --
clear-host
$run_script.Runspace = $rs

$handle = $run_script.BeginInvoke()

start-sleep 3
$max_cnt  = 10
$cnt = 0
while ($cnt -lt $max_cnt) {
   $cnt ++
    Start-Sleep -Milliseconds 1000
    $so.Progress.PerformStep()
}

password

出于调试目的,窗体上添加了带有相同处理程序的 Forward 按钮。为了使脚本的执行成为可能,窗体是从第二个 PowerShell 运行空间启动的。与 caller 参数相反,使用 Synchronized HashTable 对象进行通信。此技术广泛用于 WPF 控件。

定时器

下一个示例使用稍作修改的 Timer PowerShell 来显示正在经过的时间,而主 PowerShell 脚本继续执行一些耗时的任务。

$handle = $run_script.BeginInvoke()
foreach ($work_step_cnt in @( 1,2,3,5,6,7)) {
  Write-Output ('Doing lengthy work step {0}' -f $work_step_cnt)
  Start-Sleep -Millisecond 1000
}
Write-Output 'All Work done'
$wait_timer_step = 0
$wait_timer_max = 2

任务完成后,如果计时器仍然可见,则停止它。

while (-not $handle.IsCompleted) {
  Write-Output 'waiting on timer to finish'
  $wait_timer_step++
  Start-Sleep -Milliseconds 1000
  if ($wait_timer_step -ge $wait_timer_max) {
    $so.Progress.Value = $so.Progress.Maximum
    Write-Output 'Stopping timer'
    break
  }
}
$run_script.EndInvoke($handle)
$rs.Close()
return

timer

包含进度条和计时器的窗体完全用 PowerShell 编写。

    function GenerateForm {
      param(
        [int]$timeout_sec
      )

      @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }

      $f = New-Object System.Windows.Forms.Form
      $f.MaximumSize = $f.MinimumSize = New-Object System.Drawing.Size (220,65)
      $so.Form = $f
      $f.Text = 'Timer'
      $f.Name = 'form_main'
      $f.ShowIcon = $False
      $f.StartPosition = 1
      $f.DataBindings.DefaultDataSourceUpdateMode = 0
      $f.ClientSize = New-Object System.Drawing.Size (($f.MinimumSize.Width - 10),($f.MinimumSize.Height - 10))

      $components = New-Object System.ComponentModel.Container
      $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
      $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
      $f.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen

      $f.SuspendLayout()

      $t = New-Object System.Windows.Forms.Timer

      $p = New-Object System.Windows.Forms.ProgressBar
      $p.DataBindings.DefaultDataSourceUpdateMode = 0
      $p.Maximum = $timeout_sec
      $p.Size = New-Object System.Drawing.Size (($f.ClientSize.Width - 10),($f.ClientSize.Height - 20))
      $p.Step = 1
      $p.TabIndex = 0
      $p.Location = New-Object System.Drawing.Point (5,5)
      $p.Style = 1
      $p.Name = 'progressBar1'
      $so.Progress = $p

      $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState

      function start_timer {

        $t.Enabled = $true
        $t.Start()

      }

      $t_OnTick = {
        $p.PerformStep()
        $elapsed = New-TimeSpan -Seconds ($p.Maximum - $p.Value)
        $f.Text = ('{0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds)

        if ($p.Value -eq $p.Maximum) {
          $t.Enabled = $false
          $f.Close()
        }
      }

      $OnLoadForm_StateCorrection = {
        # Correct the initial state of the form to prevent the .Net maximized form issue - http://poshcode.org/1192
        $f.WindowState = $InitialFormWindowState
        start_timer
      }

      $elapsed = New-TimeSpan -Seconds ($p.Maximum - $p.Value)
      $f.Text = ('{0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds)

      $f.Controls.Add($p)

      $t.Interval = 1000
      $t.add_tick($t_OnTick)

      $InitialFormWindowState = $f.WindowState
      $f.add_Load($OnLoadForm_StateCorrection)
      [void]$f.ShowDialog()

    }

任务列表进度

接下来,通过结合 Progressbar 和 Timer 示例与 Task List Progress 程序集,可以为长运行的多步 PowerShell 脚本实现相同的功能。

下面提供了脚本源(脚本也可以在源 zip 中找到。解释窗体的机制并启用 Skip forward 按钮仍在进行中)。

$DebugPreference = 'Continue'

$shared_assemblies = @(
  # https://codeproject.org.cn/Articles/11588/Progress-Task-List-Control
  'ProgressTaskList.dll',
  'nunit.core.dll',
  'nunit.framework.dll'
)


$shared_assmblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'

if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {

  Write-Debug ('Using environment: {0}' -f $env:SHARED_ASSEMBLIES_PATH)
  $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
}

pushd $shared_assmblies_path

$shared_assemblies | ForEach-Object {
  $assembly = $_
  Write-Debug $assembly
  if ($host.Version.Major -gt 2) {
    Unblock-File -Path $assembly
  }
  Add-Type -Path $assembly
}
popd


# http://stackoverflow.com/questions/8343767/how-to-get-the-current-directory-of-the-cmdlet-being-executed
function Get-ScriptDirectory
{
  $Invocation = (Get-Variable MyInvocation -Scope 1).Value;
  if ($Invocation.PSScriptRoot)
  {
    $Invocation.PSScriptRoot;
  }
  elseif ($Invocation.MyCommand.Path)
  {
    Split-Path $Invocation.MyCommand.Path
  }
  else
  {
    $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
  }
}

在此版本中,将使用 ProgressTaskList.dll 的现有功能,不进行任何修改,并将程序集在 Visual Studio 中构建并放置在 $env:SHARED_ASSEMBLIES_PATH 路径下。

实际的工作步骤将在主脚本中执行,因此窗体将在单独的 Runspace 中执行。

$so = [hashtable]::Synchronized(@{
    'Title' = [string]'';
    'Visible' = [bool]$false;
    'ScriptDirectory' = [string]'';
    'Form' = [System.Windows.Forms.Form]$null;
    'DebugMessage' = '';
    'Current' = 0;
    'Previous' = 0;
    'Last' = 0;
    'Tasks' = [System.Management.Automation.PSReference];
    'Progress' = [Ibenza.UI.Winforms.ProgressTaskList]$null;
  })

在窗体的 timer 回调中,使用 $so.Current$so.Last$so.Previous 来检测何时调用放置在窗体上的 Ibenza.UI.Winforms.ProgressTaskList 对象的 NextTask()

$so.ScriptDirectory = Get-ScriptDirectory
$rs = [runspacefactory]::CreateRunspace()
$rs.ApartmentState = 'STA'
$rs.ThreadOptions = 'ReuseThread'
$rs.Open()
$rs.SessionStateProxy.SetVariable('so',$so)

$run_script = [powershell]::Create().AddScript({

在窗体中,实例化一个 System.Windows.Forms.Timer 对象来检查主脚本中执行的 Tasks 的状态。还有一个 System.Windows.Forms.Button 用于推送当前任务,其功能未完成,因此其状态被禁用。

    function ProgressbarTasklist {
      param(
        [string]$title,
        [System.Management.Automation.PSReference]$tasks_ref,
        [object]$caller
      )

      @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }

      $f = New-Object -TypeName 'System.Windows.Forms.Form'
      $so.Form = $f
      $f.Text = $title
      $t = New-Object System.Windows.Forms.Timer
      $so.DebugMessage = '"in form"'
      function start_timer {

        $t.Enabled = $true
        $t.Start()

      }

      $t_OnTick = {
        # TODO 
        # $elapsed = New-TimeSpan -Seconds ($p.Maximum - $p.Value)
        # $text = ('{0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds)
        if ($so.Current -eq $so.Last) {
          $t.Enabled = $false
          $so.DebugMessage = '"Complete"'
          $f.Close()
        } else {
          $so.DebugMessage = '"in timer"'
          if ($so.Current -gt $so.Previous) {
            $o.NextTask()
            $so.Previous = $so.Current
            $so.DebugMessage = ('Finished "{0}"' -f $so.Previous )
          }
        }
      }
      $t.Interval = 300
      $t.add_tick($t_OnTick)

      $f.Size = New-Object System.Drawing.Size (650,150)
      $f.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
      $f.AutoScaleBaseSize = New-Object System.Drawing.Size (5,14)
      $f.ClientSize = New-Object System.Drawing.Size (292,144)


      $panel = New-Object System.Windows.Forms.Panel
      $panel.BackColor = [System.Drawing.Color]::Silver
      $panel.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle

      $b = New-Object System.Windows.Forms.Button
      $b.Location = New-Object System.Drawing.Point (210,114)
      $b.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
      $b.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',7,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)

      $b.Text = 'Skip forward'
      [scriptblock]$progress = {

        if (-not $o.Visible) {
          # set the first task to 'in progress'
          $o.Visible = $true
          $so.Current = 1
          $o.Start()

        } else {
          # TODO: set the following task to 'skipped'
          $so.Current = $so.Current + 1
          $so.DebugMessage = ('Skipped "{0}"' -f $so.Current )
          $o.NextTask()
        }
      }

      $progress_click = $b.add_click
      $progress_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          if ($so.Current -eq $so.Last)
          {
            $b.Enabled = $false
            Start-Sleep -Millisecond 300
            $so.Current = $so.Current + 1
            $so.Visible = $false
          } else {
            Invoke-Command $progress -ArgumentList @()
          }

        })
      $b.Enabled = $false
      $o = New-Object -TypeName 'Ibenza.UI.Winforms.ProgressTaskList' -ArgumentList @()
      $o.BackColor = [System.Drawing.Color]::Transparent
      $o.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
      $o.Dock = [System.Windows.Forms.DockStyle]::Fill
      $o.Location = New-Object System.Drawing.Point (0,0)
      $o.Name = "progressTaskList1"
      $o.Size = New-Object System.Drawing.Size (288,159)
      $o.TabIndex = 2
      $so.Progress = $o
      $o.TaskItems.AddRange(@( [string[]]$tasks_ref.Value))

      $so.Last = $tasks_ref.Value.Count + 1 # will use 1-based index 
      $o.Visible = $false
      $panel.SuspendLayout()
      $panel.ForeColor = [System.Drawing.Color]::Black
      $panel.Location = New-Object System.Drawing.Point (0,0)
      $panel.Name = 'panel'
      $panel.Size = New-Object System.Drawing.Size (($f.Size.Width),($f.Size.Height))
      $panel.TabIndex = 1

      $panel.Controls.Add($o)
      $panel.ResumeLayout($false)
      $panel.PerformLayout()
      $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState

      $f.Controls.AddRange(@( $b,$panel))
      $f.Topmost = $True

      $so.Visible = $true
      $f.Add_Shown({
          $f.WindowState = $InitialFormWindowState
          $f.Activate()
          Invoke-Command $progress -ArgumentList @()
          start_timer
        })
      [void]$f.ShowDialog()

      $f.Dispose()
    }
    $tasks_ref = $so.Tasks
    ProgressbarTasklist -tasks_ref $tasks_ref -Title $so.Title
    Write-Output ("Processed:`n{0}" -f ($tasks_ref.Value -join "`n"))
  })

运行在默认 runspace 中的调用脚本更新 $so.Current,从而在执行适当步骤后向窗体的 timer 发出信号——目前它休眠随机时间,不超过 5 秒。此外,它会将进度消息打印到控制台,尽管良好的同步不是本示例的主要目的。假定实际工作会产生大量额外的屏幕输出,使得很难发现何时完成某个步骤。

$tasks = @(
  'Verifying cabinet integrity',
  'Checking necessary disk space',
  'Extracting files',
  'Modifying registry',
  'Installing files',
  'Removing temporary files')


$task_status = @{}

$tasks | ForEach-Object { $task_status[$_] = $null }

$so.Tasks = ([ref]$tasks)
$so.Title = 'Task List'

$run_script.Runspace = $rs

$handle = $run_script.BeginInvoke()

function PerformStep {

  param(
    [int]$step,
    [switch]$skip
  )
  $task_status[$step] = $true

  $so.Current = $step
  # can call Progress class methods across Runspaces 
  # $so.Progress.NextTask() 

}

Start-Sleep -Millisecond 100
while ($so.Visible) {
  for ($cnt = 0; $cnt -ne $tasks.Count; $cnt++) {
    $step_name = $tasks[$cnt]
    Start-Sleep -Milliseconds (Get-Random -Maximum 5000)
    PerformStep -Step $cnt
    Write-Host ('Completes step [{0}] "{1}"' -f $cnt,$step_name)
  }
  $so.Visible = $false
}
Write-Output $so.DebugMessage
# Close the progress form
$so.Form.Close()


$run_script.EndInvoke($handle)
$rs.Close()

一切完成后,窗体关闭,运行空间被销毁。

Three button prompt

如果您要修改 Ibenza.UI.Winforms.ProgressTaskList 的源代码,首先将类的设计器生成的代码存储在脚本中作为 Add-TypeTypeDefinition 参数。唯一需要的修改是从 https://www.iconfinder.com 下载合适的 16x16 图标并替换

this.imageList1.ImageStream = ((System.Windows.Forms.ImageListStreamer)(resources.GetObject("imageList1.ImageStream")))

        private string[] iconPaths = new string[] {
          @"C:\developer\sergueik\powershell_ui_samples\1420429962_216151.ico",        
          @"C:\developer\sergueik\powershell_ui_samples\1420429337_5880.ico",
          @"C:\developer\sergueik\powershell_ui_samples\1420429523_62690.ico",        	
          @"C:\developer\sergueik\powershell_ui_samples\1420429596_9866.ico"        
        } ;
...
foreach (string iconPath in this.iconPaths)
            {
                this.imageList1.Images.Add(new Icon(iconPath));
            }

下一步是重构 PowerShell 脚本,暂时移除额外的 runspace 和计时器对象,并将重点放在按钮上。

  $b = New-Object System.Windows.Forms.Button
  $b.Location = New-Object System.Drawing.Point (210,114)
  $b.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',7,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)

  $b.Text = 'forward'

  $b.add_click({

      if ($caller.Current -eq $caller.Last)
      {
        $b.Enabled = false
      } else {

        if (-not $o.Visible) {
          # set the first task to 'in progress'
          $o.Visible = $true
          $caller.Current = 1
          $o.Start()


        } else {
          # set the following task to 'in progress'
          $o.NextTask()
          $caller.Current = $caller.Current + 1
        }
      }
    })
  # original assembly
  # $i = New-Object -TypeName 'Ibenza.UI.Winforms.ProgressTaskList' -ArgumentList @()
  $o = New-Object -TypeName 'WIP.ProgressTaskList' -ArgumentList @()

在上面,引入了 $caller 对象来存储 CurrentLast 索引。

圆形进度指示器

Circle Progress

下一个示例结合了 异步 GUIProgressCircle-进度控件,以生成一个通过 PowerShell 运行空间直接调用窗体元素控制的单个进程圆形进度指示器。

窗体(不包括 ProgressCircle.ProgressCircle Add-Type)是

Add-Type -AssemblyName 'System.Windows.Forms'
Add-Type -AssemblyName 'System.Drawing'
# VisualStyles are only needed for a very few Windows Forms controls like ProgessBar
[void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms.VisualStyles')


$Form = New-Object System.Windows.Forms.Form
$l1 = New-Object System.Windows.Forms.Label
$is= New-Object System.Windows.Forms.FormWindowState
$Form.Text = 'Demo Form'
$Form.Name = 'Form'
$Form.DataBindings.DefaultDataSourceUpdateMode = 0
$Form.ClientSize = New-Object System.Drawing.Size (216,121)
# Label 
$l1.Name = 'progress_label'
$l1.Location = New-Object System.Drawing.Point (70,34)
$l1.Size = New-Object System.Drawing.Size (100,23)
$l1.Text = 'Round:'


#  progressCircle1
$c1 = New-Object -TypeName 'ProgressCircle.ProgressCircle'
$c1.Location = New-Object System.Drawing.Point (20,20)
$c1.Name = "progress_circle"
$c1.PCElapsedTimeColor1 = [System.Drawing.Color]::Chartreuse
$c1.PCElapsedTimeColor2 = [System.Drawing.Color]::Yellow
$c1.PCLinearGradientMode = [System.Drawing.Drawing2D.LinearGradientMode]::Vertical
$c1.PCRemainingTimeColor1 = [System.Drawing.Color]::Navy
$c1.PCRemainingTimeColor2 = [System.Drawing.Color]::LightBlue
$c1.PCTotalTime = 25
$c1.Size = New-Object System.Drawing.Size (47,45)
$c1.TabIndex = 3
$progress_complete = $c1.add_PCCompleted
$progress_complete.Invoke({
    param([object]$sender,[string]$message)
    # [System.Windows.Forms.MessageBox]::Show('Task completed!')
    $l1.Text = ('Task completed!')
  })


$Form.Controls.AddRange(@($l1,$c1))

$is= $Form.WindowState

$Form.add_Load({
    $Form.WindowState = $InitialFormWindowState
  })

调用者构造 System.EventArgs 对象以在 ProgressCircle.ProgressCircle 控件上执行委托,该委托会递增并更新按名称找到的相应 Label。请注意,有几种方法可以做到这一点。

$rs = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($Host)
$rs.ApartmentState = 'STA'
$rs.ThreadOptions = 'ReuseThread'
$rs.Open()

$rs.SessionStateProxy.SetVariable('Form',$Form)
$po = [System.Management.Automation.PowerShell]::Create()
$po.Runspace = $rs

$po.AddScript({
    [System.Windows.Forms.Application]::EnableVisualStyles()
    [System.Windows.Forms.Application]::Run($Form)
  })

$res = $po.BeginInvoke()

if ($PSBoundParameters['pause']) {
  Write-Output 'Pause'
  try {
    [void]$host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
  } catch [exception]{}
} else {
  Start-Sleep -Millisecond 1000
}


# subclass
$eventargs = New-Object -TypeName 'System.EventArgs'

Add-Member -InputObject $eventargs -MemberType 'NoteProperty' -Name 'Increment' -Value 0 -Force
Add-Member -InputObject $eventargs -MemberType 'NoteProperty' -Name 'Total' -Value 0 -Force

$handler = [System.EventHandler]{
  param(
    [object]$sender,
    [System.EventArgs]$e
  )
  $local:increment = $e.Increment
  $local:total = $e.Total
  $sender.Increment($local:increment)
  $sender.Text = $e.MyText
  try {
    $elems = $sender.Parent.Controls.Find('progress_label',$false)
  } catch [exception]{
  }
  if ($elems -ne $null) {
    $elems[0].Text = ('Round: {0}' -f $local:total)
  }

}

1..25 | ForEach-Object {

  $eventargs.Total = $_
  $eventargs.Increment = 1
  [void]$c1.BeginInvoke($handler,($c1,([System.EventArgs]$eventargs)))

  Start-Sleep -Milliseconds (Get-Random -Maximum 1000)

}

if ($PSBoundParameters['pause']) {
  # block PowerShell Main-Thread to leave it alive until user enter something
  Write-Output 'Pause'
  try {
    [void]$host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
  } catch [exception]{}
} else {
  Start-Sleep -Millisecond 2000
}


[System.Windows.Forms.Application]::Exit()
$po.EndInvoke($res)
$rs.Close()
$po.Dispose()

注意:要在 W2K3 上运行脚本,您必须触发另一个调用(更新的脚本可在源 zip 中找到)。

1..($total_steps ) | ForEach-Object {

  $current_step = $_
  $message = $eventargs.Text =( 'Processed {0} / {1}' -f $current_step , $total_steps )

  $eventargs.Increment = 1
  [void]$c1.BeginInvoke($handler,($c1,([System.EventArgs]$eventargs)))
  if ($host.Version.Major -eq 2) {
    $c1.Invoke(

        [System.Action[int, string]] { 
            param(
              [int]$increment, 
              [string]$message
            )
            $sender.Increment($increment) 
            try {
              $elems = $sender.Parent.Controls.Find('progress_label',$false)
            } catch [exception]{
            }
            if ($elems -ne $null) {
              $elems[0].Text = $message
            }

        },

        # Argument for the System.Action delegate scriptblock

        @(1, $message)

    )
  }
  Start-Sleep -Milliseconds (Get-Random -Maximum 1000)
}

多任务进度跟踪的泛化正在进行中。完整的示例代码在源 zip 中提供。

可以使用 Mac OS X 样式进度圆形,只需对 C# 代码进行最少的修改。

Add-Type -TypeDefinition @"

// "

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;

namespace ProgressControl
{
    public partial class CircularProgressControl : UserControl
    {
    // ... omitted most of the code
        public enum Direction
        {
            CLOCKWISE,
            ANTICLOCKWISE
        }
        public Direction Rotation { get; set; }
        private bool m_clockwise;
        public bool Clockwise 
        {
            get
            {
                return m_clockwise;
            }
            set
            {
                m_clockwise = value;
                if (m_clockwise){ 
                   this.Rotation = Direction.CLOCKWISE;
                } else { 
                   this.Rotation = Direction.ANTICLOCKWISE;
                 }
            }
        }

     // .. the rest of the class definition

    }
}
"@ -ReferencedAssemblies 'System.Windows.Forms.dll','System.Drawing.dll','System.Data.dll'

脚本的 PowerShell 部分是:

@( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
$f = New-Object System.Windows.Forms.Form
$f.AutoScaleDimensions = New-Object System.Drawing.SizeF (6.0,13.0)
$f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
$f.BackColor = [System.Drawing.Color]::LightGray
$f.ClientSize = New-Object System.Drawing.Size (170,140)

$button1 = New-Object System.Windows.Forms.Button
$cbc1 = New-Object ProgressControl.CircularProgressControl
$cbc2 = New-Object ProgressControl.CircularProgressControl
$f.SuspendLayout()

$button1.Location = New-Object System.Drawing.Point (70,80)
$button1.Name = "button1"
$button1.Size = New-Object System.Drawing.Size (75,23)
$button1.TabIndex = 0
$button1.Text = "Start"
$button1.UseVisualStyleBackColor = true
$button1.add_click.Invoke({
    param(
      [object]$sender,
      [System.EventArgs]$eventargs
    )
    if ($button1.Text -eq "Start")
    {
      $button1.Text = 'Stop'
      $cbc1.Start()
      $cbc2.Start()
    }
    else
    {
      $button1.Text = 'Start'
      $cbc1.Stop()
      $cbc2.Stop()
    }
  })


$cbc1.BackColor = [System.Drawing.Color]::Transparent
$cbc1.Interval = 60
$cbc1.Location = New-Object System.Drawing.Point (10,20)
$cbc1.MinimumSize = New-Object System.Drawing.Size (56,56)
$cbc1.Name = "circularProgressControl1"
$cbc1.Clockwise = $true
$cbc1.Size = New-Object System.Drawing.Size (56,56)
$cbc1.StartAngle = 270
$cbc1.TabIndex = 1
$cbc1.TickColor = [System.Drawing.Color]::DarkBlue

$cbc2.BackColor = [System.Drawing.Color]::Transparent
$cbc2.Interval = 60
$cbc2.Location = New-Object System.Drawing.Point (10,80)
$cbc2.MinimumSize = New-Object System.Drawing.Size (56,56)
$cbc2.Name = "$cbc2"
$cbc2.Clockwise = $false
$cbc2.Size = New-Object System.Drawing.Size (56,56)
$cbc2.StartAngle = 270
$cbc2.TabIndex = 2
$cbc2.TickColor = [System.Drawing.Color]::Yellow

$f.Controls.Add($cbc2)
$f.Controls.Add($button1)
$f.Controls.Add($cbc1)
$f.Name = "Form1"
$f.Text = 'OS X Progress Control'
$f.ResumeLayout($false)

[void]$f.ShowDialog()

osx circle progress

文件系统树视图

下一个示例将 文件系统树视图 自定义为 PowerShell。在 Add-Type -TypeDefinition 中,结合了 FileSystemTreeViewShellIcon 类的实现。

using System;
using System.IO;
using System.Windows.Forms;
using System.ComponentModel;
using System.Collections;
using System.Drawing;
using System.Runtime.InteropServices;

namespace C2C.FileSystem
{
    public class FileSystemTreeView : TreeView
    {
...  
    }
    public class ShellIcon
    {
... 
 }

}

在 PowerShell 部分,向 C2C.FileSystem.FileSystemTreeView 添加 AfterSelect 处理程序,其中选定的 TreeNode FullPath 被存储并写入文本框。$show_files_checkbox 复选框允许在运行时打开和关闭 LoadFiles

treeview

$caller = New-Object -TypeName 'Win32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
$chooser = New-Object -TypeName 'C2C.FileSystem.FileSystemTreeView' -ArgumentList ($caller)

[void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
[void][System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
[void][System.Reflection.Assembly]::LoadWithPartialName('System.Data')

# set up form
$form = New-Object System.Windows.Forms.Form
$form.Text = $title


$form.Size = New-Object System.Drawing.Size (700,450)

$panel = New-Object System.Windows.Forms.Panel


$panel1 = New-Object System.Windows.Forms.Panel
$btnDirectory = New-Object System.Windows.Forms.Button
$label1 = New-Object System.Windows.Forms.Label
$txtDirectory = New-Object System.Windows.Forms.TextBox
$treePanel = New-Object System.Windows.Forms.Panel
$panel1.SuspendLayout()
$form.SuspendLayout()

# 
# panel1
# 
$panel1.Controls.Add($btnDirectory)
$panel1.Controls.Add($label1)
$panel1.Controls.Add($txtDirectory)
$panel1.Dock = [System.Windows.Forms.DockStyle]::Top
$panel1.Location = New-Object System.Drawing.Point (0,0)
$panel1.Name = 'panel1'
$panel1.Size = New-Object System.Drawing.Size (681,57)
$panel1.TabIndex = 0

$show_files_checkbox = New-Object System.Windows.Forms.CheckBox
$show_files_checkbox.Location = New-Object System.Drawing.Point (515,27)
$show_files_checkbox.Size = New-Object System.Drawing.Size (120,20)
$show_files_checkbox.Text = 'Files'

$panel1.Controls.Add($show_files_checkbox)
$show_files_checkbox.add_click({ if ($show_files_checkbox.Checked -eq $true) { $chooser.ShowFiles = $true } else { $chooser.ShowFiles = $false } })


# 
# btnDirectory
# 
$btnDirectory.Location = New-Object System.Drawing.Point (560,27)
$btnDirectory.Name = "btnDirectory"
$btnDirectory.Size = New-Object System.Drawing.Size (60,21)
$btnDirectory.TabIndex = 2
$btnDirectory.Text = 'Select'
$btnDirectory.add_click({ if ($caller.Data -ne $null) { $form.Close() } })

# 
# label1
# 
$label1.Location = New-Object System.Drawing.Point (9,9)
$label1.Name = 'label1'
$label1.Size = New-Object System.Drawing.Size (102,18)
$label1.TabIndex = 1
$label1.Text = 'Selection:'

# 
# txtDirectory
# 
$txtDirectory.Location = New-Object System.Drawing.Point (9,27)
$txtDirectory.Name = "txtDirectory"
$txtDirectory.Size = New-Object System.Drawing.Size (503,20)
$txtDirectory.TabIndex = 0
$txtDirectory.Text = ""

# 
# treePanel
# 
$treePanel.Dock = [System.Windows.Forms.DockStyle]::Fill
$treePanel.Location = New-Object System.Drawing.Point (0,57)
$treePanel.Name = "treePanel"
$treePanel.Size = New-Object System.Drawing.Size (621,130)
$treePanel.TabIndex = 1

$treePanel.Controls.Add($chooser)
$chooser.ShowFiles = $false
$chooser.Dock = [System.Windows.Forms.DockStyle]::Fill
$chooser.Add_AfterSelect({ $txtDirectory.Text = $caller.Data = $chooser.Data })
$chooser.Load('C:\')
# Form1
# 
$form.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
$form.ClientSize = New-Object System.Drawing.Size (621,427)
$form.Controls.Add($treePanel)
$form.Controls.Add($panel1)
$form.Name = 'Form1'
$form.Text = 'Demo Chooser'
$panel1.ResumeLayout($false)
$form.ResumeLayout($false)
$form.Add_Shown({ $form.Activate() })
$form.KeyPreview = $True
$form.Add_KeyDown({

    if ($_.KeyCode -eq 'Escape') { $caller.Data = $null }
    else { return }
    $form.Close()
  })

[void]$form.ShowDialog([win32window ]($caller))

$form.Dispose()
Write-Output $caller.Data

完整的脚本源代码可在源 zip 文件中找到。

嵌入 XAML

password

设计 WPF XAML 更简单。

Add-Type -AssemblyName PresentationFramework
[xml]$xaml =
@"
<?xml version="1.0"?>
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Row GridSplitter Example">
  <StackPanel Height="Auto">
    <Grid Height="400">
      <Grid.RowDefinitions>
        <RowDefinition Height="50*"/>
        <RowDefinition Height="50*"/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>
      <Button Background="gray" Grid.Column="0"
      Grid.Row="0" x:Name="button00" HorizontalAlignment="Stretch"
      VerticalAlignment="Stretch" Content="Quentin Tarantino"/>
      <Button Background="gray" Grid.Column="0" Grid.Row="1"
      x:Name="button01" HorizontalAlignment="Stretch"
      VerticalAlignment="Stretch" Content="Larry Dimmick"/>
      <Button Background="gray" Grid.Column="1" Grid.Row="0"
      x:Name="button10" HorizontalAlignment="Stretch"
      VerticalAlignment="Stretch" Content="Steve Buscemi"/>
      <Button Background="gray" Grid.Column="1" Grid.Row="1"
      x:Name="button11" HorizontalAlignment="Stretch"
      VerticalAlignment="Stretch" Content="Tim Roth"/>
    </Grid>
  </StackPanel>
</Window>
"@

现在,System.Windows.Window 不接受 IWin32Window 参数。

$colors = @{
  'Steve Buscemi' = ([System.Windows.Media.Colors]::Pink);
  'Larry Dimmick' = ([System.Windows.Media.Colors]::White);
  'Quentin Tarantino' = ([System.Windows.Media.Colors]::Orange);
  'Tim Roth' = ([System.Windows.Media.Colors]::Brown);
}

$result = @{ }

$DebugPreference = 'Continue'
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$target=[Windows.Markup.XamlReader]::Load($reader )
$target.ShowDialog() | out-null
# $result | format-table

对于简单的行为,一种将结果传回脚本的方法是通过在脚本中定义并在事件处理程序中可见的 $result 哈希变量。

foreach ($button in @("button01" , "button00", "button10", "button11")) {
  $control=$target.FindName($button)
  $eventMethod=$control.add_click
  $eventMethod.Invoke({
     param(
       [Object] $sender,
       [System.Windows.RoutedEventArgs ] $eventargs
     )
    $who = $sender.Content.ToString()
    $color = $colors[$who ]
    # $target.Title=("You will be  Mr. {0}" -f  $color)
    $sender.Background = new-Object System.Windows.Media.SolidColorBrush($color)
    $result[ $who  ] = $true
    write-debug $who
  })
}

此示例很简单——同一个事件处理程序附加到 XAML 流中的每个可点击元素。发送者的详细信息存储在 $result 中,同时为了提供视觉提示,代码会更改 $sender 的背景。

...即时

另一个例子是,可以使用以下代码片段通过 XAML 动态生成 ComboBox 源列表:

$items = @(
   'Apple' ,
   'Banana' ,
   'Orange' ,
   'Pineapple' ,
   'Plum'
   )
$selected = @{ }
$context = @'
<window height="60" title="Window1" width="200" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <stackpanel>
    <combobox iseditable="False" margin="5" name="comboBox">
'@
$cnt = 1
$items | foreach-object {  $name = "Item_${cnt}" ; $cnt ++ ; $context +="<comboboxitem content="$_" name="${name}">" }
$context += @'
        </comboboxitem></combobox>
    </stackpanel>
</window>
'@


Add-Type -AssemblyName PresentationFramework
[xml]$xaml = $context

Clear-Host
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$target=[Windows.Markup.XamlReader]::Load($reader)
$handler = {
      param ([object] $sender,  # System.Windows.Controls.ComboboxItem
                                # http://msdn.microsoft.com/en-us/library/system.windows.controls.comboboxitem_properties%28v=vs.110%29.aspx
             [System.Windows.RoutedEventArgs] $eventargs )
      $sender.Background = [ System.Windows.Media.Brushes]::Red
      $target.Title = ( 'Added {0} ' -f $sender.Content )
      $selected[ $sender.Content  ]  = $true
  }

此代码为项目选择提供了最小但清晰的视觉反馈。

foreach ($item in ("Item_1", "Item_5", "Item_2","Item_3","Item_4") ){
  $combobox_item_control = $target.FindName( $item )
  $eventargsventMethod2 = $combobox_item_control.add_Selected
  $eventargsventMethod2.Invoke( $handler )
  $combobox_item_control = $null
}

产生

TreeView

并以 PowerShell 的方式打印选定的结果。

$target.ShowDialog() | out-null
write-output 'Selected items:'$items | where-object {$selected.ContainsKey( $_ ) }

TreeView

更多

值得注意的是,您可以在纯 XAML 中设计一个非常丰富的用户界面,同时保持实际选择处理的简单性。

例如,通过(在很大程度上)重复之前的练习,但在面板上绘制 3 个填充颜色的箭头多边形。

Add-Type -AssemblyName PresentationFramework
[xml]$xaml = @"
// .... code below
"@

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="100" Width="200" Title="Window1">
  <Canvas Height="100" Width="200" Name="Canvas1">
    <!-- Draws a triangle with a blue interior. -->
    <Polygon Points="0,0 0,30 0,10 30,10 30,-10 45,10 30,30 30,20 0,20 0,0 30,0 30,10 0,10" Fill="Blue" Name="Polygon1" Canvas.Left="40" Canvas.Top="30" Canvas.ZIndex="40"/>
    <Polygon Points="0,0 0,30 0,10 30,10 30,-10 45,10 30,30 30,20 0,20 0,0 30,0 30,10 0,10" Fill="Green" Name="Polygon2" Canvas.Left="70" Canvas.Top="30" Canvas.ZIndex="30"/>
    <Polygon Points="0,0 0,30 0,10 30,10 30,-10 45,10 30,30 30,20 0,20 0,0 30,0 30,10 0,10" Fill="Red" Name="Polygon3" Canvas.Left="100" Canvas.Top="30" Canvas.ZIndex="20"/>
  </Canvas>
</Window>

并在事件处理程序中更改鼠标选定箭头的颜色和 ZIndex,并在窗口标题中反映选定的多边形名称。

Clear-Host

$polygon_data = @{}
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$target = [Windows.Markup.XamlReader]::Load($reader)
$canvas = $target.FindName("Canvas1")
function save_orig_design{
  param ([String] $name)
  $control = $target.FindName($name)
  return @{
      'fill'   =  ( $control.Fill.Color );
      'ZIndex' =  ( [System.Windows.Controls.Canvas]::GetZIndex($control) )
      }
  }
  $polygon_data['Polygon1'] = (save_orig_design('Polygon1'))
  $polygon_data['Polygon2'] = (save_orig_design('Polygon2'))
  $polygon_data['Polygon3'] = (save_orig_design('Polygon3'))

# TODO :
# $canvas.Add_Initialized ...
function restore_orig {
  param ( [String] $name )
  $control = $target.FindName( $name )
  $color = [System.Windows.Media.ColorConverter]::ConvertFromString( [String] $polygon_data[$name]['fill'] )
  $control.Fill = new-Object System.Windows.Media.SolidColorBrush( $color )
  [System.Windows.Controls.Canvas]::SetZIndex($control, [Object] $polygon_data[$name]['ZIndex'])
}
$handler = {
param (
    [Object]  $sender,
    [System.Windows.Input.MouseButtonEventArgs] $e  )
  @('Polygon1', 'Polygon2', 'Polygon3') | % { restore_orig( $_) }
  # Highlight sender
  $sender.Fill = new-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Colors]::Orange)
  # uncomment to reveal a distortion
  # $sender.Stroke = new-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Colors]::Black)
  # Bring sender to front
  [System.Windows.Controls.Canvas]::SetZIndex($sender,[Object]100)
  $target.Title="Hello $($sender.Name)"
}
foreach ($item in ('Polygon1', 'Polygon2', 'Polygon3') ){
  $control = $target.FindName($item)
  $eventMethod = $control.add_MouseDown
  $eventMethod.Invoke( $handler )
  $control = $null
 }
$eventMethod.Invoke($handler)
$target.ShowDialog() | out-null

可以获得独特的效果。

TreeView

TreeView

TreeView

但是设计代码隐藏可能很困难。安排 PowerShell 和 WPF 之间的通信(well documented)并且看起来是一项相当具有挑战性的任务。

连接 WPF 事件

要安排 PowerShell 运行空间之间的交互,您可以创建一个可选的强类型 synchronized 对象,并创建一个额外的 RunSpace 来执行 WPF 事件。

#requires -version 2
$so = [hashtable]::Synchronized(@{
    'Result'  = '';
    'Window'  = [System.Windows.Window] $null ;
    'TextBox' = [System.Windows.Controls.TextBox] $null ;
    })
$so.Result = ''
$rs =[runspacefactory]::CreateRunspace()
$rs.ApartmentState = 'STA'
$rs.ThreadOptions = 'ReuseThread'
$rs.Open()
$rs.SessionStateProxy.SetVariable('so', $so)

接下来,将 XAML 处理代码包装在 Add-Script 方法中。

$run_script = [PowerShell]::Create().AddScript({

Add-Type -AssemblyName PresentationFramework
[xml]$xaml = @"
<window height="100" title="Example with TextBox" width="300" x:name="Window" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <stackpanel height="100" width="300">
          <textblock fontsize="14" fontweight="Bold" text="A spell-checking TextBox:">
        <textbox acceptsreturn="True" acceptstab="True" fontsize="14" margin="5" spellcheck.isenabled="True" textwrapping="Wrap" x:name="textbox">

        </textbox>

  </textblock></stackpanel>
</window>
"@

$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$target = [Windows.Markup.XamlReader]::Load( $reader )
$so.Window  = $target
$handler = {
    param (
    [Object]  $sender,
    [System.Windows.Controls.TextChangedEventArgs] $eventargs
    )
    $so.Result  = $sender.Text
}
$control = $target.FindName("textbox")
$so.TextBox = $control

$event = $control.Add_TextChanged
$event.Invoke( $handler )

$eventMethod.Invoke($handler)
$target.ShowDialog() | out-null
})

然后设计通过共享对象 $so 操作的访问器函数。请注意,某些必须可访问的属性不能在另一个线程上进行评估。调用线程无法访问此对象,因为另一个线程拥有它,该异常仅在运行时引发。

function send_text {
    Param (
        $content,
        [switch] $append
    )
    # WARNING - uncommenting the following line leads to exception
    # $so.Textbox = $so.Window.FindName("textbox")

    # NOTE - host-specific method signature:
    $so.Textbox.Dispatcher.invoke([System.Action]{

        if ($PSBoundParameters['append_content']) {
            $so.TextBox.AppendText($content)
        } else {
            $so.TextBox.Text = $content
        }
        $so.Result = $so.TextBox.Text
    }, 'Normal')
}

function close_dialog {
    $so.Window.Dispatcher.invoke([action]{
       $so.Window.Close()
    }, 'Normal')
}

最后,主脚本调用动态创建的脚本并控制窗体。

$run_script.Runspace = $rs
Clear-Host

$data = $run_script.BeginInvoke()

# TODO - synchronize properly

start-sleep 1
write-host $so.Result
send_text -Content 'The qick red focks jumped over the lasy brown dog.'
$cnt = 10
[bool] $done = $false
while (($cnt  -ne 0 ) -and -not $done) {
  write-output ('Text: {0} ' -f $so.Result )
  if ($so.Result -eq 'The quick red fox jumped over the lazy brown dog.' ){
    $done = $true;
  }
  else {
    start-sleep 10
  }
  $cnt --
}
close_dialog

if ( -not $done ){
    write-output 'Time is up!'
} else {
    write-output 'Well done!'
}

此示例初始化文本时包含一些拼写错误。

TypeText1

并等待用户修复拼写错误。一旦文本被更正或超时到期,窗体将关闭并打印摘要。

TypeText2

由于 PowerShell/WPF 通信所需的代码略微复杂,建议从更简单的示例开始,并在所有事件处理程序按预期执行后转换为最终形式。可以相对快速地以这种方式转换较早的示例。

您还可以通过窗体安排双向通信,例如,在脚本的稍作修改的版本中将当前数据加载到复选框工具提示中。

function Get-ScriptDirectory
{
    $Invocation = (Get-Variable MyInvocation -Scope 1).Value;
    if($Invocation.PSScriptRoot)
    {
        $Invocation.PSScriptRoot;
    }
    Elseif($Invocation.MyCommand.Path)
    {
        Split-Path $Invocation.MyCommand.Path
    }
    else
    {
        $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
    }
}
$so = [hashtable]::Synchronized(@{
        'Result'  = [string] '';
        'ScriptDirectory'  = [string] '';

    'Window'  = [System.Windows.Window] $null ;
    'Control' = [System.Windows.Controls.ToolTip] $null ;
    'Contents' = [System.Windows.Controls.TextBox] $null ;
    'NeedData' = [bool] $false ;
    'HaveData' = [bool] $false ;

    })
$so.ScriptDirectory = Get-ScriptDirectory

$so.Result = ''
$rs =[runspacefactory]::CreateRunspace()
$rs.ApartmentState = 'STA'
$rs.ThreadOptions = 'ReuseThread'
$rs.Open()
$rs.SessionStateProxy.SetVariable('so', $so)
$run_script = [PowerShell]::Create().AddScript({

Add-Type -AssemblyName PresentationFramework
[xml]$xaml = @"
<window height="190" removed="LightGray" title="About WPF" width="168" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
        <canvas>

        <img opacity=".7" source="$('{0}\{1}' -f $so.ScriptDirectory, 'clock.jpg' )" width="150" />
          <image.tooltip>
            <tooltip name="tooltip">
            <stackpanel>
              <label background="Blue" fontweight="Bold" foreground="White">
                The CheckBox
              </label>
              <stackpanel orientation="Horizontal">
                <img margin="2" name="hourglass" source="$('{0}\{1}' -f $so.ScriptDirectory, 'hourglass.jpg' )" visibility="Collapsed" width="20" />
              <textblock name="tooltip_textbox" padding="10" textwrapping="WrapWithOverflow" width="200">
                please wait...
              </textblock>
              </stackpanel>
            </stackpanel>
           </tooltip>
          </image.tooltip>

        </canvas>
</window>
"@

$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$target = [Windows.Markup.XamlReader]::Load($reader)
$so.Window  = $target
$control = $target.FindName("tooltip")
$so.Indicator = $target.FindName("hourglass")
$contents = $target.FindName("tooltip_textbox")
$so.Control = $control
$so.Contents = $contents
$handler_opened = {
  param (
    [Object]  $sender,
    [System.Windows.RoutedEventArgs] $eventargs
    )
        $so.Contents.Text = 'please wait...'
        $so.Indicator.Visibility = 'Visible'
    $so.NeedData  = $true
        $so.Result = ''
}
$handler_closed = {
  param (
    [Object]  $sender,
    [System.Windows.RoutedEventArgs] $eventargs
    )
        $so.HaveData = $false
    $so.NeedData = $false
}

[System.Management.Automation.PSMethod] $event_opened = $control.Add_Opened
[System.Management.Automation.PSMethod] $event_closed = $control.Add_Closed

$event_opened.Invoke( $handler_opened )
$event_closed.Invoke( $handler_closed)
$target.ShowDialog() | out-null
})

function send_text {
    Param (
        $content,
        [switch] $append
    )

    # NOTE - host-specific method signature:
    $so.Indicator.Dispatcher.invoke([System.Action]{
        $so.Indicator.Visibility = 'Collapsed'
    }, 'Normal')
    $so.Contents.Dispatcher.invoke([System.Action]{

        if ($PSBoundParameters['append_content']) {
            $so.Contents.AppendText($content)
        } else {
            $so.Contents.Text = $content
        }
    $so.Result = $so.Contents.Text
    }, 'Normal')
}

$run_script.Runspace = $rs
Clear-Host

$handle = $run_script.BeginInvoke()
While (-Not $handle.IsCompleted) {
    Start-Sleep -Milliseconds 100
    if ($so.NeedData -and -not $so.HaveData){
      write-output ('Need to provide data' )
      Start-Sleep -Milliseconds 10
      send_text -Content (Date)
      write-output ('Sent {0}' -f $so.Result )
      $so.HaveData = $true
    }
}
$run_script.EndInvoke($handle)
$rs.Close()

Toolbox1

在此示例中,使用 ToolTip Opened,Closed 事件通过 SynchronizedNeedData 标志设置和清除到顶层脚本,然后将文本更改为 please wait 并显示沙漏直到数据准备好。数据渲染再次在 send_text 中执行。请注意,send_text 函数现在调用 Dispatcher 两次,视觉反馈不完美。每次鼠标离开和重新进入 Tooltip 激活区域时,都会请求并提供新数据。

Toolbox2

树视图

纯文本

TreeView

在启动 PowerShell 脚本(例如,用于指标收集)时,通常需要从某个分组方式中选择特定的节点。

function PromptTreeView
{
     Param(
    [String] $title,
    [String] $message)

  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections.Generic')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Text')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Data')
  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title
  $t = New-Object  System.Windows.Forms.TreeView
  $components = new-object System.ComponentModel.Container
  $f.SuspendLayout();
  $t.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);

  $i = new-Object System.Windows.Forms.ImageList($components)
  $i.Images.Add([System.Drawing.SystemIcons]::Application)
  $t.ImageList = $i


  $t.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
        -bor [System.Windows.Forms.AnchorStyles]::Left) `
        -bor [System.Windows.Forms.AnchorStyles]::Right)
  $t.ImageIndex = -1
  $t.Location = new-object System.Drawing.Point(4, 5)
  $t.Name = "treeFood"
  $t.SelectedImageIndex = -1
  $t.Size = new-object System.Drawing.Size(284, 256)
  $t.TabIndex = 1;
  $t_AfterSelect =  $t.add_AfterSelect
  $t_AfterSelect.Invoke({
    param(
    [Object] $sender,
    [System.Windows.Forms.TreeViewEventArgs] $eventargs
    )
    if ($eventargs.Action -eq [System.Windows.Forms.TreeViewAction]::ByMouse)
    {
        write-host $eventargs.Node.FullPath
    }
})

 $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
 $f.ClientSize = new-object System.Drawing.Size(292, 266)
 $f.Controls.AddRange(@( $t))
 $f.Name = "TreeViewExample"
 $f.Text = "TreeView Example"
 $f_Load = $f.add_Load
 $f_Load.Invoke({
   param(
    [Object] $sender,
    [System.EventArgs] $eventargs
   )

    $node = $t.Nodes.Add("Fruits")
    $node.Nodes.Add("Apple")
    $node.Nodes.Add("Peach")

    $node = $t.Nodes.Add("Vegetables")
    $node.Nodes.Add("Tomato")
    $node.Nodes.Add("Eggplant")
})

  $f.ResumeLayout($false)

  $f.Name = 'Form1'
  $f.Text = 'TreeView Sample'
  $t.ResumeLayout($false)
  $f.ResumeLayout($false)
  $f.StartPosition = 'CenterScreen'
  $f.KeyPreview = $false

  $f.Topmost = $True
  $caller = New-Object Win32Window -ArgumentList([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

  $f.Add_Shown( { $f.Activate() } )

  [Void] $f.ShowDialog([Win32Window ] ($caller) )

  $t.Dispose()
  $f.Dispose()
}

高级

自定义图标

通过添加 ScriptDirectory 属性……

    private string _script_directory;
    public string ScriptDirectory
    {
        get { return _script_directory; }
        set { _script_directory = value; }
    }

……并更新 PromptTreeView 签名以接收 $caller,脚本可以通过 $caller 将其位置传递给窗体。

$caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
$caller.ScriptDirectory = Get-ScriptDirectory

$result = PromptTreeView 'Items'  $caller

function Get-ScriptDirectory
{
#  implementation omitted
# http://stackoverflow.com/questions/8343767/how-to-get-the-current-directory-of-the-cmdlet-being-executed
}

后者将能够加载自定义图标。

   try {
     $script_path  = $caller.ScriptDirectory
    } catch [Exception] {
           # slurp the exception - debug code omitted
    }
    if ($script_path -eq '' -or $script_path -eq $null )  {
      $script_path = get-location
   }
  foreach ($n in @(1,2,3)){
     $image_path =  ( '{0}\color{1}.gif' -f $script_path ,  $n )
     $image = [System.Drawing.Image]::FromFile($image_path)
     $i.Images.Add($image)
  }

TreeView

并为各个节点使用不同的图标。使用相同的技术,调用脚本可以描述要为每个节点渲染的图标。

$node = $t.Nodes.Add("Fruits")
    $apple = $node.Nodes.Add("Apple")
    $apple.ImageIndex  = 1

    $node.Nodes.Add("Peach")

    $node = $t.Nodes.Add("Vegetables")
    $tomato = $node.Nodes.Add("Tomato")
    $tomato.ImageIndex  = 2 

后台工作者

此脚本的下一个迭代还包含一个更详细的事件处理程序版本。该示例可用于处理在用户为远程位置提供对象(存在延迟)时可能需要的时间消耗型验证。在不强迫用户退出对话框的情况下进行此类验证可能是可取的。在下面的代码中,窗体 TreeView 元素单击会实例化一个 BackgroundWorker 以在单独的线程上处理操作。窗体目前不提供视觉提示 $worker 已启动,尽管这是可能的。

因此,模态对话框仍然可以——由于事件处理代码是 100% PowerShell,因此无需安排脚本和窗体之间复杂的同步——每次窗体希望通过调用相关 PowerShell cmdlet 来运行数据验证时,它都可以直接进行。

$worker = new-object System.ComponentModel.BackgroundWorker
    $worker.WorkerReportsProgress = $false;
    $worker.WorkerSupportsCancellation = $false;
    $worker_DoWork  = $worker.Add_DoWork
    $worker_DoWork.Invoke({
        param(
        [Object] $sender,
        [System.Windows.Forms.DoWorkEventArgs] $eventargs
    )
    })

所有工作都在 Completed 事件处理程序中完成。在这个例子中,文本文件“etc/hosts”在记事本中打开,并且线程等待用户关闭记事本。这是 Windows.Forms 的标准示例/推荐实践,除了 Backgroundworker 通常用 C# 实现。很高兴发现它与 PowerShell 代码开箱即用。

$worker_RunWorkerCompleted = $worker.Add_RunWorkerCompleted
    $worker_RunWorkerCompleted.Invoke({
        param(
        [Object] $sender,
        [System.ComponentModel.RunWorkerCompletedEventArgs] $eventargs
        )
       $child_proc  = [System.Diagnostics.Process]::Start('notepad',"$env:windir\system32\drivers\etc\hosts")
       $child_proc.WaitForExit()
})

制表

人们真的希望将树视图安装到选项卡中,而不是文本框中。这将使选项选择完全由鼠标驱动,并且是可能的。

与早期示例的微小区别是 treeview 重绘后的事件名称——对于 tabPage 它是 VisibleChangedEvent

#
$panel1.add_VisibleChanged({
   param(
    [Object]$sender,
    [System.EventArgs]$eventargs
   )
    $t1.SuspendLayout()
    $t1.Nodes.Clear()
    $node = $t1.Nodes.Add('Target Environment')
    $node.Nodes.Add('Database Server')
    $node.Nodes.Add('Application Server')
    $sites = $node.Nodes.Add('Web Server')
    $sites.Nodes.Add('Site 1')
    $sites.Nodes.Add('Site 2')
    $sites.Nodes.Add('Site 3')
    $t1.ResumeLayout($false)
    $t1.PerformLayout()
})

完整源代码如下。

function TabsWithTreeViews(
    [String] $title,
  [Object] $caller
    ){

  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')

  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title

  $panel2 = new-object System.Windows.Forms.TabPage
  $panel1 = new-object System.Windows.Forms.TabPage
  $tab_contol1 = new-object System.Windows.Forms.TabControl
  $panel2.SuspendLayout()
  $panel1.SuspendLayout()
  $tab_contol1.SuspendLayout()
  $f.SuspendLayout()

  $panel2.Location = new-object System.Drawing.Point(4, 22)
  $panel2.Name = "tabPage2"
  $panel2.Padding = new-object System.Windows.Forms.Padding(3)
  $panel2.Size = new-object System.Drawing.Size(259, 352)
  $panel2.AutoSize  = $true
  $panel2.TabIndex = 1
  $panel2.Text = "Source Node"

  $l1 = New-Object System.Windows.Forms.Label
  $l1.Location = New-Object System.Drawing.Point(8,12)
  $l1.Size = New-Object System.Drawing.Size(220,16)
  $l1.Text = 'enter status message here'

  $l1.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 8, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);

  $groupBox1 = New-Object System.Windows.Forms.GroupBox

  $groupBox1.SuspendLayout()

  $groupBox1.Controls.AddRange( @($l1 ))
  $groupBox1.Location = New-Object System.Drawing.Point(8,230)
  $groupBox1.Name = 'groupBox1'
  $groupBox1.Size = New-Object System.Drawing.Size(244,32)
  $groupBox1.TabIndex = 0
  $groupBox1.TabStop = $false
  $groupBox1.Text = 'status'


  $panel2.Controls.Add($groupBox1)

  $t2 = New-Object  System.Windows.Forms.TreeView
  $t2.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);

  $i = new-Object System.Windows.Forms.ImageList($components)
  $i.Images.Add([System.Drawing.SystemIcons]::Application)
  $t2.ImageList = $i

  $t2.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
  -bor [System.Windows.Forms.AnchorStyles]::Left) `
  -bor [System.Windows.Forms.AnchorStyles]::Right)
  $t2.ImageIndex = -1
  $t2.Location = new-object System.Drawing.Point(4, 5)
  $t2.Name = "treeFood"
  $t2.SelectedImageIndex = -1
  $t2.Size = new-object System.Drawing.Size(284, 224)

  $t2.AutoSize  = $true
  $t2.TabIndex = 1;
  $panel2.Controls.AddRange(@($t2))

 # http://msdn.microsoft.com/en-us/library/system.windows.forms.tabpage.visiblechanged%28v=vs.110%29.aspx
   $panel2.add_VisibleChanged({
     param(
      [Object] $sender,
      [System.EventArgs] $eventargs
      )
    $t2.SuspendLayout()
    $t2.Nodes.Clear()
    $node = $t2.Nodes.Add('Source Environment')
    $server = $node.Nodes.Add('Test Server')
    $databases = $server.Nodes.Add('Databases')
    $server.Nodes.Add('DB 1')
    $server.Nodes.Add('DB 2')

    $server.Nodes.Add('Application')
    $sites = $server.Nodes.Add('IIS Web Sites')

    $sites.Nodes.Add('Site 1')
    $sites.Nodes.Add('Site 2')
    $sites.Nodes.Add('Site 3')
    $t2.ResumeLayout($false)
    $t2.PerformLayout()
})

  $panel1.Location = new-object System.Drawing.Point(4, 22)
  $panel1.Name = "tabPage1"
  $panel1.Padding = new-object System.Windows.Forms.Padding(3)
  $panel1.Size = new-object System.Drawing.Size(259, 252)
  $panel1.TabIndex = 0
  $panel1.Text = "Destination Node"

  $t1 = New-Object  System.Windows.Forms.TreeView
  $t1.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);

  $t1.ImageList = $i

  $t1.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
  -bor [System.Windows.Forms.AnchorStyles]::Left) `
  -bor [System.Windows.Forms.AnchorStyles]::Right)
  $t1.ImageIndex = -1
  $t1.Location = new-object System.Drawing.Point(4, 5)
  $t1.Name = "treeFood"
  $t1.SelectedImageIndex = -1
  $t1.Size = new-object System.Drawing.Size(284, 224)

  $t1.AutoSize  = $true
  $t1.TabIndex = 1;
  $panel1.Controls.AddRange(@($t1))

   $panel1.add_VisibleChanged({
       param(
          [Object] $sender,
          [System.EventArgs] $eventargs
       )
    $t1.SuspendLayout()
    $t1.Nodes.Clear()
    $node = $t1.Nodes.Add('Target Environment')
    $node.Nodes.Add('Database Server')
    $node.Nodes.Add('Application Server')
    $sites = $node.Nodes.Add('Web Server')
    $sites.Nodes.Add('Site 1')
    $sites.Nodes.Add('Site 2')
    $sites.Nodes.Add('Site 3')
    $t1.ResumeLayout($false)
    $t1.PerformLayout()
})

  $tab_contol1.Controls.Add($panel1)
  $tab_contol1.Controls.Add($panel2)

  $tab_contol1.Location = new-object System.Drawing.Point(13, 13)
  $tab_contol1.Name = "tabControl1"
  $tab_contol1.SelectedIndex = 1
  $tab_contol1.Size = new-object System.Drawing.Size(267, 288)
  $tab_contol1.TabIndex = 0

  $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
  $f.ClientSize = new-object System.Drawing.Size(292, 308)
  $f.Controls.Add($tab_contol1)
  $panel2.ResumeLayout($false)
  $panel2.PerformLayout()
  $panel1.ResumeLayout($false)
  $tab_contol1.ResumeLayout($false)
  $f.ResumeLayout($false)

  $f.Topmost = $true


  $f.Add_Shown( { $f.Activate() } )
  $f.KeyPreview = $True


  [Void] $f.ShowDialog([Win32Window ] ($caller) )

  $f.Dispose()
}

TreeView

代码仍在进行中,目的是使用状态标签进行验证警告,并使用工作进程进行更深入的环境验证。

TreeView

下拉组合框

要在 V4 之前的 PowerShell 环境中管理 PowerShell Desired State Configuration 配置管理器 - 节点 - 提供程序 - 属性输入,您可能希望使用 combobox 扩展 treeview 。例如,可以使用 Mattman206 的自定义带组合框下拉节点的树视图控件。在编译类并将程序集放置在 SHARED_ASSEMBLIES_PATH 文件夹后,脚本会加载它,并在表单加载事件处理过程中自由混合 System.Windows.Forms.TreeNodeDropDownTreeView.DropDownTreeNode 节点:Mattman206 可以这样做。在编译类并将程序集放置在 SHARED_ASSEMBLIES_PATH 文件夹后,脚本会加载它,

制表

人们真的希望将树视图安装到选项卡中,而不是文本框中。这将使选项选择完全由鼠标驱动,并且是可能的。

与早期示例的微小区别是 treeview 重绘后的事件名称——对于 tabPage 它是 VisibleChangedEvent

#
$panel1.add_VisibleChanged({
   param(
    [Object]$sender,
    [System.EventArgs]$eventargs
   )
    $t1.SuspendLayout()
    $t1.Nodes.Clear()
    $node = $t1.Nodes.Add('Target Environment')
    $node.Nodes.Add('Database Server')
    $node.Nodes.Add('Application Server')
    $sites = $node.Nodes.Add('Web Server')
    $sites.Nodes.Add('Site 1')
    $sites.Nodes.Add('Site 2')
    $sites.Nodes.Add('Site 3')
    $t1.ResumeLayout($false)
    $t1.PerformLayout()
})

完整源代码如下。

function TabsWithTreeViews(
    [String] $title,
  [Object] $caller
    ){

  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
  [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')

  $f = New-Object System.Windows.Forms.Form
  $f.Text = $title

  $panel2 = new-object System.Windows.Forms.TabPage
  $panel1 = new-object System.Windows.Forms.TabPage
  $tab_contol1 = new-object System.Windows.Forms.TabControl
  $panel2.SuspendLayout()
  $panel1.SuspendLayout()
  $tab_contol1.SuspendLayout()
  $f.SuspendLayout()

  $panel2.Location = new-object System.Drawing.Point(4, 22)
  $panel2.Name = "tabPage2"
  $panel2.Padding = new-object System.Windows.Forms.Padding(3)
  $panel2.Size = new-object System.Drawing.Size(259, 352)
  $panel2.AutoSize  = $true
  $panel2.TabIndex = 1
  $panel2.Text = "Source Node"

  $l1 = New-Object System.Windows.Forms.Label
  $l1.Location = New-Object System.Drawing.Point(8,12)
  $l1.Size = New-Object System.Drawing.Size(220,16)
  $l1.Text = 'enter status message here'

  $l1.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 8, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);

  $groupBox1 = New-Object System.Windows.Forms.GroupBox

  $groupBox1.SuspendLayout()

  $groupBox1.Controls.AddRange( @($l1 ))
  $groupBox1.Location = New-Object System.Drawing.Point(8,230)
  $groupBox1.Name = 'groupBox1'
  $groupBox1.Size = New-Object System.Drawing.Size(244,32)
  $groupBox1.TabIndex = 0
  $groupBox1.TabStop = $false
  $groupBox1.Text = 'status'


  $panel2.Controls.Add($groupBox1)

  $t2 = New-Object  System.Windows.Forms.TreeView
  $t2.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);

  $i = new-Object System.Windows.Forms.ImageList($components)
  $i.Images.Add([System.Drawing.SystemIcons]::Application)
  $t2.ImageList = $i

  $t2.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
  -bor [System.Windows.Forms.AnchorStyles]::Left) `
  -bor [System.Windows.Forms.AnchorStyles]::Right)
  $t2.ImageIndex = -1
  $t2.Location = new-object System.Drawing.Point(4, 5)
  $t2.Name = "treeFood"
  $t2.SelectedImageIndex = -1
  $t2.Size = new-object System.Drawing.Size(284, 224)

  $t2.AutoSize  = $true
  $t2.TabIndex = 1;
  $panel2.Controls.AddRange(@($t2))

 # http://msdn.microsoft.com/en-us/library/system.windows.forms.tabpage.visiblechanged%28v=vs.110%29.aspx
   $panel2.add_VisibleChanged({
     param(
      [Object] $sender,
      [System.EventArgs] $eventargs
      )
    $t2.SuspendLayout()
    $t2.Nodes.Clear()
    $node = $t2.Nodes.Add('Source Environment')
    $server = $node.Nodes.Add('Test Server')
    $databases = $server.Nodes.Add('Databases')
    $server.Nodes.Add('DB 1')
    $server.Nodes.Add('DB 2')

    $server.Nodes.Add('Application')
    $sites = $server.Nodes.Add('IIS Web Sites')

    $sites.Nodes.Add('Site 1')
    $sites.Nodes.Add('Site 2')
    $sites.Nodes.Add('Site 3')
    $t2.ResumeLayout($false)
    $t2.PerformLayout()
})

  $panel1.Location = new-object System.Drawing.Point(4, 22)
  $panel1.Name = "tabPage1"
  $panel1.Padding = new-object System.Windows.Forms.Padding(3)
  $panel1.Size = new-object System.Drawing.Size(259, 252)
  $panel1.TabIndex = 0
  $panel1.Text = "Destination Node"

  $t1 = New-Object  System.Windows.Forms.TreeView
  $t1.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);

  $t1.ImageList = $i

  $t1.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
  -bor [System.Windows.Forms.AnchorStyles]::Left) `
  -bor [System.Windows.Forms.AnchorStyles]::Right)
  $t1.ImageIndex = -1
  $t1.Location = new-object System.Drawing.Point(4, 5)
  $t1.Name = "treeFood"
  $t1.SelectedImageIndex = -1
  $t1.Size = new-object System.Drawing.Size(284, 224)

  $t1.AutoSize  = $true
  $t1.TabIndex = 1;
  $panel1.Controls.AddRange(@($t1))

   $panel1.add_VisibleChanged({
       param(
          [Object] $sender,
          [System.EventArgs] $eventargs
       )
    $t1.SuspendLayout()
    $t1.Nodes.Clear()
    $node = $t1.Nodes.Add('Target Environment')
    $node.Nodes.Add('Database Server')
    $node.Nodes.Add('Application Server')
    $sites = $node.Nodes.Add('Web Server')
    $sites.Nodes.Add('Site 1')
    $sites.Nodes.Add('Site 2')
    $sites.Nodes.Add('Site 3')
    $t1.ResumeLayout($false)
    $t1.PerformLayout()
})

  $tab_contol1.Controls.Add($panel1)
  $tab_contol1.Controls.Add($panel2)

  $tab_contol1.Location = new-object System.Drawing.Point(13, 13)
  $tab_contol1.Name = "tabControl1"
  $tab_contol1.SelectedIndex = 1
  $tab_contol1.Size = new-object System.Drawing.Size(267, 288)
  $tab_contol1.TabIndex = 0

  $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
  $f.ClientSize = new-object System.Drawing.Size(292, 308)
  $f.Controls.Add($tab_contol1)
  $panel2.ResumeLayout($false)
  $panel2.PerformLayout()
  $panel1.ResumeLayout($false)
  $tab_contol1.ResumeLayout($false)
  $f.ResumeLayout($false)

  $f.Topmost = $true


  $f.Add_Shown( { $f.Activate() } )
  $f.KeyPreview = $True


  [Void] $f.ShowDialog([Win32Window ] ($caller) )

  $f.Dispose()
}

TreeView

代码仍在进行中,目的是使用状态标签进行验证警告,并使用工作进程进行更深入的环境验证。

TreeView

三态树视图

PowerShell 允许管理员管理大量数据,因此通常希望更新复杂的对象集合,而三态树视图可能是首选。下面的示例包装了 RikTheVeggie 的自定义 TriStateTreeView 类。实际上,源代码 TriStateTreeView.cs 未经修改地嵌入到脚本中——唯一需要修改的部分是测试示例中的 PopulateTreetriStateTreeView1_BeforeExpand 方法——这些方法已被转换为 PowerShell 语义。

function populateTree {
  param(
    [System.Windows.Forms.TreeNodeCollection]$parent_nodes,
    [string]$text
  )
  # Add 5 nodes to the current node.  Every other node will have a child

  for ($i = 0; $i -lt 5; $i++) {

    [System.Windows.Forms.TreeNode]$tn = New-Object System.Windows.Forms.TreeNode (("{0}{1}" -f $text,($i + 1)))

    if (($i % 2) -eq 0) {
      # add a 'dummy' child node which will be replaced at runtime when the parent is expanded
      $tn.Nodes.Add("")

    }
    # There is no need to set special properties on the node if adding it at form creation or when expanding a parent node.
    # Otherwise, set 
    # tn.StateImageIndex = [int]([RikTheVeggie.TriStateTreeView.CheckedState]::UnChecked)
    $parent_nodes.Add($tn)
  }
}


  $t = New-Object -typeName 'RikTheVeggie.TriStateTreeView'
  $t.Dock = [System.Windows.Forms.DockStyle]::Fill
  $t.Location = New-Object System.Drawing.Point (0,0)
  $t.Name = 'triStateTreeView1'
  $t.Size = New-Object System.Drawing.Size (284,262)
  $t.TabIndex = 0
  populateTree -parent_nodes $t.Nodes -text ""
  $treeview_BeforeExpand = $t.add_BeforeExpand
  $treeview_BeforeExpand.Invoke({
    param(
      [object]$sender,
      [System.Windows.Forms.TreeViewCancelEventArgs]$e
    )
    # A node in the tree has been selected
    [System.Windows.Forms.TreeView]$tv = $sender
    $tv.UseWaitCursor = $true
    if (($e.Node.Nodes.Count -eq 1) -and ($e.Node.Nodes[0].Text -eq '')) {
      # This is a 'dummy' node.  Replace it with actual data
      $e.Node.Nodes.RemoveAt(0)
	  populateTree -parent_nodes $e.Node.Nodes -text $e.Node.Text
    }
    $tv.UseWaitCursor = $false

  })

选定节点的信息存储在 $caller 对象中,并在 AfterCheck 事件处理程序中传递给脚本,该事件处理程序是概念验证。

 

$treeview_AfterCheck = $t.add_AfterCheck
  $treeview_AfterCheck.Invoke({
      param(
        [object]$sender,
        [System.Windows.Forms.TreeViewEventArgs]$eventargs
      )
        # [System.Windows.Forms.MessageBox]::Show($eventargs.Node.Text);
		if ( $eventargs.Node.Checked ) {
		  if ($eventargs.Node.Text -ne '') {
            $caller.Message += ('{0},' -f $eventargs.Node.Text)
		  }
		}  
    })

tristateTreeView

一个更实用的示例结合了 三态树视图MSDN 中的示例,以在按钮单击处理程序中收集所有“已选中”的节点,在 PowerShell 中将以下示例数据加载到 RikTheVeggie.TriStateTreeView(大部分 System.Windows.Forms.TreeView 操作在脚本的 C# 部分)。

$tree = @{

  'Vegetable' = @{
                  'Allium sativum' = @{
                                       'garlic' = '$null';
                                      };
                  'Phaseolus' = @{
                                  'green bean' = '$null';
                                  'haricot bean' = '$null';
                                  'French bean' = '$null';
                                  'runner bean' = '$null';
                                  'Lima bean' = '$null';
                                 };
  };
  'Fruit' = @{
              'Hesperidium' = @{
                                'Lemon' = '$null';
                                'Grapefruit' = '$null';
                                'Lime' = '$null';
                                'Orange' = '$null';
                               };
              'Pepo' = '$null';
              'True berry' = @{
                               'Lucuma' = '$null';
                               'Blueberry' = '$null';
...

——当然,它可以处理任意深度嵌套的树,例如

$deeply_nested_tree =
@{
  'a' = @{
          'aa' = @{
                   'aaa' = @{
                             'aaaa' = @{
                                        'aaaaa' = '$null';
                                       }
                            };
                   'aab' = @{
                             'aaba' = '$null';
                            };
                   'aac' = '$null';
                  };
          'ab' = @{
                   'aba' = @{
                             'abaa' = '$null';
                             'abab' = '$null'
                            }
                  };
          'ac' = @{
                   'aca' = '$null';
                   'acb' = '$null';
                   'acc' = '$null';
                   'acd' = '$null';
                   'ace' = '$null';
                   'acf' = '$null';
                   'acg' = '$null';
                   'ach' = '$null';
                  };
          'ad' = '$null';
         };
  'b' = @{
          'ba' = '$null'
          'bb' = '$null';
          'bc' = '$null';
          'bd' = '$null';
          'be' = '$null';
         };
  'c ' = '$null';
 }

借助以下函数:

function populateTree {
  param(
    [Object]$Object,
    [System.Windows.Forms.TreeNode]$parent_node
  )
 
  [System.Windows.Forms.TreeNode]$new_node

  if ( $Object -is [hashtable] ) {
    foreach ( $pair in $Object.GetEnumerator() ){
      # Add node
      if ($parent_node -eq $null) { 
        $new_node = $t.treeView1.Nodes.Add($pair.Key)
      } else {
        $new_node  = $parent_node.Nodes.Add($pair.Key)
      }
      # Recursion is here
      populateTree -object $pair.Value -parent_node $new_node 
    }
  }

}

并通过 HashSet 处理选择,示例代码摘自 www.java2s.com

if ($caller.Count -gt 0) {
  Write-Output 'Selection is : '
  $caller.GetEnumerator() | ForEach-Object { Write-Output $_ }
} else { 
  Write-Output 'Nothing was selected.'

}

treeView1_AfterCheck 正确更新节点选中状态。

    private void treeView1_AfterCheck(object sender, TreeViewEventArgs e)
    {

        // http://stackoverflow.com/questions/5478984/treeview-with-checkboxes-in-c-sharp
        if (isDrawing) return;
        isDrawing = true;
        
        if (!e.Node.Checked)
        {

            if (e.Node.Parent!= null && !HasCheckedChildNodes(e.Node.Parent))
            {
                try
                {
                    e.Node.Parent.Checked = false;
                }
                catch { }
            }
        } 
        
        try
        {
            checkNodes(e.Node, e.Node.Checked);
        }
        finally
        {
            isDrawing = false;
        }
    }

Show Selected Items 按钮收集带有选中(祖先)子项的节点。在 PowerShell 中编码时,删除事件处理程序效果不佳,因此所有内容都保留在 C# 中。

private void showCheckedNodesButton_Click(object sender, EventArgs e)
    {
        treeView1.BeginUpdate();
        treeView1.CollapseAll();
        treeView1.BeforeExpand += checkForCheckedChildren;

        // Prevent Nodes without checked children from expanding 
        treeView1.ExpandAll();

        // Remove the checkForCheckedChildren event handler 
        treeView1.BeforeExpand -= checkForCheckedChildren;

        // Enable redrawing of treeView1.
        treeView1.EndUpdate();
    }

制表项的树

下一个示例使用了漂亮的 TreeTabControl。制表项树 来用于 PowerShell。

可以在 TreeTab/TreeTab/TreeTabControl.xaml.cs 类中添加一个小的公共方法,使 PowerShell 可以使用该类。

        /// <summary>
        /// Converts the string parameter to TreeItemType enumeration.
        /// </summary>
        /// <param name="_typestring">string</param>
        /// <returns>_type</returns>
        public TreeItem.TREEITEM_TYPE ConvertType(string _typestring ){
         TreeItem.TREEITEM_TYPE _type;
            if (String.Compare(_typestring, "MAIN", true) == 0)
            	_type = TreeItem.TREEITEM_TYPE.MAIN;         
            else
            	_type = TreeItem.TREEITEM_TYPE.GROUP;
            return _type;
        }

因为

 public enum TREEITEM_TYPE
        {
            MAIN,
            GROUP
        }

对 PowerShell 不可访问。

您几乎不修改原始容器 XAML。

<?xml version="1.0"?>
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:custom="clr-namespace:TreeTab;assembly=TreeTab" Title="Window1" Margin="0,0,0,0" Height="244" Width="633">
  <Grid x:Name="Container">
    <Grid.RowDefinitions>
      <RowDefinition Height="30"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>
      <Button x:Name="Hide_Tree" Grid.Column="1">Hide Tree</Button>
      <Button x:Name="Show_Tree" Grid.Column="0">Show Tree</Button>
    </Grid>
    <Grid x:Name="Container2" Grid.Row="1" Margin="5,5,5,5">
      <StackPanel x:Name="TreeTabContainer"></StackPanel>
    </Grid>
  </Grid>
</Window>

TreeView

PowerShell 脚本初始化了管道代码。

$shared_assemblies = @(
  'TreeTab.dll',
  'nunit.framework.dll'
)
[void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')

$shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'

if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
  $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
}

pushd $shared_assemblies_path
$shared_assemblies | ForEach-Object { Unblock-File -Path $_; Add-Type -Path $_ }
popd


Clear-Host


$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$target = [Windows.Markup.XamlReader]::Load($reader)

编译类并将程序集放置在 SHARED_ASSEMBLIES_PATH 文件夹后,将 TreeTab.TreeTabControl 的实例放置在 StackPanel 中。

$t = New-Object -TypeName 'TreeTab.TreeTabControl'

$c = $target.FindName('TreeTabContainer')
$t.IsTreeExpanded = $true
$t.Name = 'treeTab'
[void]$t.HideTree()
[void]$t.AddTabItem('Global','Global',$false,$t.ConvertType('MAIN'),'')
[void]$t.AddTabItem('Staging_Environment','Staging Environment',$false,$t.ConvertType('GROUP'),'')
[void]$t.AddTabItem('Test_Environment','Test Environment',$false,$t.ConvertType($t.ConvertType('GROUP')),'')

[TreeTab.TreeTabItemGroup]$tp0 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('Staging_Environment')
[TreeTab.TreeTabItem]$tItem = $t.AddTabItem('Certificates','Certificates',$false,$t.ConvertType('MAIN'),$tp0)
[void]$t.AddTabItem('IIS_Web_Sites','IIS Web Sites',$false,$t.ConvertType('GROUP'),$tp0)
[void]$t.AddTabItem('Databases','Databases',$false,$t.ConvertType('GROUP'),$tp0)

[TreeTab.TreeTabItemGroup]$tp02 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('Databases')
[void]$t.AddTabItem('DB_1','DB 1',$true,$t.ConvertType('MAIN'),$tp02)
[void]$t.AddTabItem('DB_2','DB 2',$true,$t.ConvertType('MAIN'),$tp02)

[TreeTab.TreeTabItemGroup]$tp03 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('IIS_Web_Sites')
[void]$t.AddTabItem('Site_1','Site 1',$true,$t.ConvertType('MAIN'),$tp03)
[void]$t.AddTabItem('Site_2','Site 2',$true,$t.ConvertType('MAIN'),$tp03)
[void]$t.AddTabItem('Site_3','Site 3',$true,$t.ConvertType('MAIN'),$tp03)
[void]$t.AddTabItem('Site_4','Site 4',$true,$t.ConvertType('MAIN'),$tp03)


[TreeTab.TreeTabItemGroup]$tp01 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('Test_Environment')
[TreeTab.TreeTabItem]$t23 = $t.AddTabItem('Certificates1','Certificates',$false,$t.ConvertType('MAIN'),$tp01)
[void]$t.AddTabItem('IIS_Web_Sites2','IIS Web Sites',$false,$t.ConvertType('GROUP'),$tp01)
[void]$t.AddTabItem('Databases2','Databases',$false,$t.ConvertType('GROUP'),$tp01)


[TreeTab.TreeTabItemGroup]$tp12 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('Databases2')
[void]$t.AddTabItem('DB_11','DB 1',$true,$t.ConvertType('MAIN'),$tp12)
[void]$t.AddTabItem('DB_12','DB 2',$true,$t.ConvertType('MAIN'),$tp12)

[TreeTab.TreeTabItemGroup]$tp13 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('IIS_Web_Sites2')
[void]$t.AddTabItem('Site_11','Site 1',$true,$t.ConvertType('MAIN'),$tp13)
[void]$t.AddTabItem('Site_12','Site 2',$true,$t.ConvertType('MAIN'),$tp13)
[void]$t.AddTabItem('Site_13','Site 3',$true,$t.ConvertType('MAIN'),$tp13)
[void]$t.AddTabItem('Site_14','Site 4',$true,$t.ConvertType('MAIN'),$tp13)


[void]$t.ShowTree()
[void]$c.AddChild($t)

$target.FindName("Hide_Tree").add_click.Invoke({
    [void]$t.HideTree()
  })
$target.FindName("Show_Tree").add_click.Invoke({
    [void]$t.ShowTree()
  })

$target.ShowDialog() | Out-Null

该类自动化了标签导航。接下来是填充选项卡标准 WPF 输入并提供特定于领域的 PING。

例如,给定

[xml]$parent_markup = @"

<pre lang="xml">
<?xml version="1.0"?>
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Margin="5,5,5,5" Height="310" Width="420">
<ScrollViewer>
    <WrapPanel>
  <Grid x:Name="LayoutRoot">
  </Grid>
     </WrapPanel>
  </ScrollViewer>
</Window>

"@

[xml]$child_markup = @"

<?xml version="1.0"?>
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <StackPanel.Resources>
    <Style TargetType="{x:Type TextBox}">
      <Setter Property="Margin" Value="0,10,0,0"/>
    </Style>
  </StackPanel.Resources>
  <Label x:Name="lblNumberOfTargetHits" HorizontalAlignment="Center">Input:</Label>
  <TextBox Width="120" x:Name="txtTargetKeyFocus" FontSize="12"/>
  <TextBox x:Name="txtTargetFocus" TextWrapping="Wrap" FontSize="12"/>
</StackPanel>

"@

嵌套控件的实现方式与

$parent_reader = (New-Object System.Xml.XmlNodeReader $parent_markup)
$parent_target = [Windows.Markup.XamlReader]::Load($parent_reader)
$LayoutRoot = $parent_target.FindName("LayoutRoot")
$child_reader = (New-Object System.Xml.XmlNodeReader $child_markup)
$child_target = [Windows.Markup.XamlReader]::Load($child_reader)
$LayoutRoot.add_Loaded.Invoke({
    $LayoutRoot.Children.Add($child_target)
  })

要运行 WPF 控件事件处理程序中的代码,请确保控件可以通过其标记 x:Name 属性找到,例如 $child,而不是 $parent

$target = $child_target
$control = $target.FindName("txtTargetKeyFocus")

$handler_got_keyboard_focus = {
  param(
    [object]$sender,
    [System.Windows.Input.KeyboardFocusChangedEventArgs]$e
  )
  $source = $e.Source
  $source.Background = [System.Windows.Media.Brushes]::LightBlue
  $source.Clear()

}
$handler_lost_keyboard_focus = {
  param(
    [object]$sender,
    [System.Windows.Input.KeyboardFocusChangedEventArgs]$e
  )
  $source = $e.Source
  $source.Background = [System.Windows.Media.Brushes]::White
}

[System.Management.Automation.PSMethod]$event_got_keyboard_focus = $control.Add_GotKeyboardFocus
[System.Management.Automation.PSMethod]$event_lost_keyboard_focus = $control.Add_LostKeyboardFocus

$event_got_keyboard_focus.Invoke($handler_got_keyboard_focus)
$event_lost_keyboard_focus.Invoke($handler_lost_keyboard_focus)
$control = $null

继续处理其余控件。

注意:借助 System.Management.Automation.TypeAccelerators 程序集,您可以避免在脚本中键入完整的类名。

$ta = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')

Add-Type -AssemblyName 'PresentationCore','PresentationFramework' -Passthru |
Where-Object IsPublic |
ForEach-Object {
  $_class = $_
  try {
    $ta::Add($_class.Name,$_class)
  } catch {
    ( 'Failed to add {0} accelerator resolving to {1}' -f $_class.Name ,   $_class.FullName )
  }
}

借助上面的代码,以下片段

# http://poshcode.org/5730
[Window]@{
  Width = 310
  Height = 110
  WindowStyle = 'SingleBorderWindow'
  AllowsTransparency = $false
  TopMost = $true
  Content = & {
    $c1 = [StackPanel]@{
      Margin = '5'
      VerticalAlignment = 'Center'
      HorizontalAlignment = 'Center'
      Orientation='Horizontal'
    }

 $t = [textblock]@{}
$t.AddChild([label]@{
Margin = '5'
VerticalAlignment = 'Center'
HorizontalAlignment = 'Center'
FontSize = '11'
FontFamily = 'Calibri'
Foreground = 'Black'
Content = 'Enter Password:'
}
)
$c1.AddChild($t)
$c1.AddChild(
[passwordbox]@{
Name = 'passwordBox'
PasswordChar = '*'
VerticalAlignment = 'Center'
Width = '120'
}
)
$c1.AddChild(
[button]@{
Content = 'OK'
IsDefault = 'True'
Margin = '5'
Name = 'button1'
Width = '50'
VerticalAlignment = 'Center'
}
)
,$c1} | ForEach-Object {
  $_.Add_MouseLeftButtonDown({
      $this.DragMove()
    })
  $_.Add_MouseRightButtonDown({
      $this.Close()
    })
  $_.ShowDialog() | Out-Null
}

产生类似的效果

<?xml version="1.0"?>
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Margin="5,5,5,5" Height="110" Width="310">

 <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Orientation="Horizontal"  VerticalAlignment="Center" HorizontalAlignment = "Center">
   <TextBlock Margin="5" FontSize = "11" FontFamily = "Calibri">
             Enter Password:
         </TextBlock>
   <PasswordBox Name="passwordBox" PasswordChar="*" VerticalAlignment="Center" Width="120"/>
   <Button Content="OK" IsDefault="True" Margin="5" Name="button1"  Width="50" VerticalAlignment="Center"/>
 </StackPanel>
</Window>

在大多数情况下,这不会在事件处理程序中产生歧义。

系统托盘通知图标

假设脚本正在运行一系列步骤,具有详细的日志记录,并且需要很长时间才能完成。自然地,您可以生成一个 Windows 系统托盘通知图标,该图标会指示正在进行的进程在做什么。关键是如何组织代码,以便控件保留在主脚本中。

通过最少的修改,ScriptIT 提供的系统托盘通知图标示例,您可以让主脚本通过气球提示消息和控制台显示其状态,并且构建日志文件用于渲染托盘图标菜单并向其传递其他信息。

TreeView

#requires -version 2
Add-Type -AssemblyName PresentationFramework

function Get-ScriptDirectory
{
    $Invocation = (Get-Variable MyInvocation -Scope 1).Value;
    if($Invocation.PSScriptRoot)
    {
        $Invocation.PSScriptRoot;
    }
    Elseif($Invocation.MyCommand.Path)
    {
        Split-Path $Invocation.MyCommand.Path
    }
    else
    {
        $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
    }
}
$so = [hashtable]::Synchronized(@{
        'Result'  = [string] '';
        'ConfigFile'  = [string] '';
        'ScriptDirectory'  = [string] '';
    'Form'  = [System.Windows.Forms.Form] $null ;
    'NotifyIcon' = [System.Windows.Controls.ToolTip] $null ;
    'ContextMenu' = [System.Windows.Forms.ContextMenu] $null ;
    })
$so.ScriptDirectory = Get-ScriptDirectory
$so.Result = ''
$rs =[runspacefactory]::CreateRunspace()
$rs.ApartmentState = 'STA'
$rs.ThreadOptions = 'ReuseThread'
$rs.Open()
$rs.SessionStateProxy.SetVariable('so', $so)

$run_script = [PowerShell]::Create().AddScript({

[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')

$f = New-Object System.Windows.Forms.Form
$so.Form  = $f
$notify_icon = New-Object System.Windows.Forms.NotifyIcon
$so.NotifyIcon = $notify_icon
$context_menu = New-Object System.Windows.Forms.ContextMenu
$exit_menu_item = New-Object System.Windows.Forms.MenuItem
$AddContentMenuItem = New-Object System.Windows.Forms.MenuItem

$build_log = ('{0}\{1}' -f $so.ScriptDirectory, 'build.log' )

function Read-Config {
  $context_menu.MenuItems.Clear()
  if(Test-Path $build_log){
    $ConfigData = Get-Content $build_log
    $i = 0
    foreach($line in $ConfigData){
      if($line.Length -gt 0){
        $line = $line.Split(",")
        $Name = $line[0]
        $FilePath = $line[1]
        # Powershell style function invocation syntax
        $context_menu | Build-ContextMenu -index $i -text $Name -Action $FilePath
        $i++
      }
    }
  }

# Create an Exit Menu Item
$exit_menu_item.Index = $i+1
$exit_menu_item.Text = 'E&xit'
$exit_menu_item.add_Click({
$f.Close()
$notify_icon.visible = $false
})

$context_menu.MenuItems.Add($exit_menu_item) | Out-Null
}

function new-scriptblock([string]$textofscriptblock)
{
$executioncontext.InvokeCommand.NewScriptBlock($textofscriptblock)
}

# construct objects from the build log file and fill the context Menu
function Build-ContextMenu {
  param (
        [int]$index = 0,
        [string]$Text,
        [string] $Action
  )
begin
{
$menu_item = New-Object System.Windows.Forms.MenuItem
}
process
{
# Assign the Context Menu Object from the pipeline to the ContexMenu var
$ContextMenu = $_
}
end
{
# Create the Menu Item$menu_item.Index = $index
$menu_item.Text = $Text
$scriptAction = $(new-scriptblock "Invoke-Item $Action")
$menu_item.add_Click($scriptAction)
$ContextMenu.MenuItems.Add($menu_item) | Out-Null
}
}
# http://bytecookie.wordpress.com/2011/12/28/gui-creation-with-powershell-part-2-the-notify-icon-or-how-to-make-your-own-hdd-health-monitor/

$notify_icon.Icon = ('{0}\{1}' -f $so.ScriptDirectory, 'sample.ico' )
#
$notify_icon.Text = 'Context Menu Test'
# Assign the Context Menu
$notify_icon.ContextMenu = $context_menu
$f.ContextMenu = $context_menu

# Control Visibility and state of things
$notify_icon.Visible = $true
$f.Visible = $false
$f.WindowState = 'minimized'
$f.ShowInTaskbar = $false
$f.add_Closing({ $f.ShowInTaskBar = $False })
$context_menu.Add_Popup({Read-Config})
$f.ShowDialog()
})

function send_text {
    Param (
        [String] $title = 'script',
        [String] $message,
        [int]    $timeout = 10 ,
        [switch] $append
    )

    $so.NotifyIcon.ShowBalloonTip($timeout, $title , $message, [System.Windows.Forms.ToolTipIcon]::Info)
    write-output  -InputObject  ( '{0}:{1}' -f $title,  $message)
}

# -- main program --
clear-host
$run_script.Runspace = $rs

$cnt = 0
$total = 4
$handle = $run_script.BeginInvoke()

start-sleep 1

send_text -title 'script' -message 'Starting...' -timeout 10
$so.ConfigFile = $build_log = ('{0}\{1}' -f $so.ScriptDirectory, 'build.log' )
set-Content -path $build_log -value ''

While (-Not $handle.IsCompleted -and $cnt -lt $total) {
  start-sleep -Milliseconds 10000
  $cnt ++
  send_text -title 'script' -message ("Finished {0} of {1} items..."  -f $cnt, $total  )  -timeout 10
  write-output ("Subtask {0} ..." -f $cnt ) | out-file -FilePath $build_log -Append -encoding ascii
}

$so.Form.Close()

$run_script.EndInvoke($handle) | out-null
$rs.Close()
write-output 'All finished'

TreeView

Selenium 测试

下一个示例演示了如何从 PowerShell 执行 Selenium WebDriver 事务。这个示例还有很多代码需要添加,但已经开发的部分Hopefully 是值得一看的。这里选择了一个简单的事务进行说明。它已从以下 MS Test 示例转换而来。

using System;
using System.Linq.Expressions;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Activities.UnitTesting;
using Moq;

using OpenQA.Selenium;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Support.UI;
using OpenQA.Selenium.IE;
using OpenQA.Selenium.PhantomJS;
using OpenQA.Selenium.Safari;
namespace SeleniumTests
{
    [TestClass]
    public class SeleniumTest
    {

    private static IWebDriver driver;
    private static StringBuilder verificationErrors = new StringBuilder();
        private string baseURL;
        private bool acceptNextAlert = true;

        [ClassCleanup()]
        public static void MyClassCleanup() {
            try {
        driver.Quit();
            } catch (Exception) {
                // Ignore errors if unable to close the browser
            }
            Assert.AreEqual("", verificationErrors.ToString());
        }

        [TestInitialize()]
        public void MyTestInitialize()
        {
         // DesiredCapabilities capability = DesiredCapabilities.PhantomJSDriver();
         // error CS0117: 'OpenQA.Selenium.Remote.DesiredCapabilities' dos not contain a definition for 'PhantomJSDriver'
         // DesiredCapabilities capability = DesiredCapabilities.Firefox();
         // driver = new RemoteWebDriver(new Uri("http://127.0.0.1:4444/wd/hub"), capability );
         // driver = new PhantomJSDriver();
         driver = new SafariDriver();
         Assert.IsNotNull(driver );

         driver.Url =  baseURL = "http://www.wikipedia.org";
         driver.Manage().Timeouts().ImplicitlyWait( TimeSpan.FromSeconds(10 )) ;
         verificationErrors = new StringBuilder();

        }

        [TestCleanup()]
        public void MyTestCleanup() {

    }

        [TestMethod]
        public void Test()
        {

            // Arrange
            driver.Navigate().GoToUrl(baseURL + "/");
            WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)) ;

            // Act
            IWebElement queryBox = driver.FindElement(By.Id("searchInput"));
            queryBox.Clear();
            queryBox.SendKeys("Selenium");
        queryBox.SendKeys(Keys.ArrowDown);
            queryBox.Submit();
            driver.FindElement(By.LinkText("Selenium (software)")).Click();
            // Assert
            Assert.IsTrue(driver.Title.IndexOf("Selenium (software)") > -1, driver.Title);
        }
    }
}

而这反过来又基本上是一个带有 MS Test 装饰的Selenium IDE 记录。

使用与本文档中其他示例类似的方法将 PowerShell 转换为 PowerShell——主要通过查阅 API 文档

该脚本使用 PhantomeJS Selenium 驱动程序进行快速测试运行,并使用真实的 Firefox 浏览器进行彻底运行。

所有标准的 Selenium C# 客户端 API dll 都放置在 SHARED_ASSEMBLIES_PATH 环境指向的文件夹中。

$shared_assemblies =  @(
    'WebDriver.dll',
    'WebDriver.Support.dll',
    'Selenium.WebDriverBackedSelenium.dll',
    'Moq.dll'
)

$shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
pushd $shared_assemblies_path
$shared_assemblies | foreach-object { Unblock-File -Path $_ ; Add-Type -Path  $_ }
popd

当然,如果存在业务逻辑层或封装低级 WebDriver 调用的 DSL,则可以从 C# 编译为独立的程序集 DLL,并以类似的方式提供给 PowerShell。

$testSuite = [System.Reflection.AssemblyName]::GetAssemblyName('${assembly_path}\BusinessTestSuite.dll')

$framework = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom(
'${assembly_path}\BusinessSpecificWebDriverFramework.dll')

为避免复制 Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll,而是从安装它的机器加载,并使熟悉的 断言调用在脚本中可用,以下代码执行快速发现。为简单起见,只显示 Microsoft Test Agent InstallLocation 注册表键扫描,需要尝试其他键,请注意 Visual Studio Express Edition 不安装此 dll,而 Enterprise 版本会安装几个副本。

function read_registry{
  param ([string] $registry_path,
         [string] $package_name

)

pushd HKLM:
cd -path $registry_path
$settings = get-childitem -Path . | where-object { $_.Property  -ne $null } | where-object {$_.name -match  $package_name } |   select-object -first 1
$values = $settings.GetValueNames()

if ( -not ($values.GetType().BaseType.Name -match  'Array' ) ) {
  throw 'Unexpected result type'
}
$result = $null
$values | where-object {$_ -match 'InstallLocation'} | foreach-object {$result = $settings.GetValue($_).ToString() ; write-debug $result}

popd
$result

}

$shared_assemblies =  @(
    'Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll'
)

$shared_assemblies_path = (  "{0}\{1}" -f   ( read_registry -registry_path '/HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersion/Uninstall' -package_name '{6088FCFB-2FA4-3C74-A1D1-F687C5F14A0D}' ) , 'Common7\IDE\PublicAssemblies' )
$shared_assemblies_path =
pushd $shared_assemblies_path
$shared_assemblies | foreach-object { Unblock-File -Path $_ ; Add-Type -Path  $_ }
popd

[Microsoft.VisualStudio.TestTools.UnitTesting.Assert]::AreEqual("true", (@('true','false') | select-object -first 1) )

根据开关,脚本初始化 phantom 或真实浏览器驱动……

if ($PSBoundParameters['browser']) {

  Try {
    $connection = (New-Object Net.Sockets.TcpClient)
    $connection.Connect('127.0.0.1',4444)
    $connection.Close()
    }
  catch {
    $selemium_driver_folder = 'c:\java\selenium'
    start-process -filepath 'C:\Windows\System32\cmd.exe' -argumentlist "start cmd.exe /c ${selemium_driver_folder}\hub.cmd"
    start-process -filepath 'C:\Windows\System32\cmd.exe' -argumentlist "start cmd.exe /c ${selemium_driver_folder}\node.cmd"
    start-sleep 10
  }

  $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Firefox()
  $uri = [System.Uri]('http://127.0.0.1:4444/wd/hub')
  $driver = new-object OpenQA.Selenium.Remote.RemoteWebDriver($uri , $capability)
} else {
  $phantomjs_executable_folder = 'C:\tools\phantomjs'
  $driver = new-object OpenQA.Selenium.PhantomJS.PhantomJSDriver($phantomjs_executable_folder)
  $driver.Capabilities.SetCapability('ssl-protocol', 'any' );
  $driver.Capabilities.SetCapability('ignore-ssl-errors', $true);
  $driver.capabilities.SetCapability("takesScreenshot", $false );
  $driver.capabilities.SetCapability("userAgent",
              "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.7 Safari/534.34")
}

无需显式启动 PhantomJS 驱动程序。

最后,测试开始(Get-ScriptDirectory 和 Assert 的实现未显示,可以在附加的源 zip 和作者的 github 存储库中找到)。

# http://selenium.googlecode.com/git/docs/api/dotnet/index.html
[void]$driver.Manage().Timeouts().ImplicitlyWait( [System.TimeSpan]::FromSeconds(10 ))
[string]$baseURL = $driver.Url = 'http://www.wikipedia.org';
$driver.Navigate().GoToUrl(('{0}/' -f $baseURL ))
[OpenQA.Selenium.Remote.RemoteWebElement]$queryBox = $driver.FindElement([OpenQA.Selenium.By]::Id('searchInput'))

$queryBox.Clear()
$queryBox.SendKeys('Selenium')
$queryBox.SendKeys([OpenQA.Selenium.Keys]::ArrowDown)
$queryBox.Submit()
$driver.FindElement([OpenQA.Selenium.By]::LinkText('Selenium (software)')).Click()
$title =  $driver.Title
assert -Script { ($title.IndexOf('Selenium (software)') -gt -1 ) } -message $title

假定测试失败,脚本会导航到标识浏览器的 URL 并截屏。

$driver.Navigate().GoToUrl("https://www.whatismybrowser.com/")
[OpenQA.Selenium.Screenshot]$screenshot = $driver.GetScreenshot()
$screenshot_path = $env:SCREENSHOT_PATH
$screenshot.SaveAsFile(('{0}\{1}' -f $screenshot_path, 'a.png' ), [System.Drawing.Imaging.ImageFormat]::Png)

Browser

并完成测试运行。

try {
  $driver.Quit()
} catch [Exception] {
  # Ignore errors if unable to close the browser
}

您可能会通过适当的 CreateRunspace 调用引入一个单独的脚本,并开发 Synchronized 对象以允许从连接到主脚本的单独 PowerShell 运行空间控制 $driver.GetScreenshot 调用的调用(这目前正在进行中),方式类似于先前示例中控制的系统托盘通知图标。

脚本的 Selenium RC 版本将加载不同的库并切换到 NunitAsserts

$shared_assemblies =  @(
    'ThoughtWorks.Selenium.Core.dll',
    'nunit.core.dll',
    'nunit.framework.dll'
)

并调用不同的方法。

$verificationErrors = new-object System.Text.StringBuilder
$selenium = new-object Selenium.DefaultSelenium('localhost', 4444, '*firefox', 'http://www.wikipedia.org/')
$selenium.Start()
$selenium.Open('/')
$selenium.Click('css=strong')
$selenium.WaitForPageToLoad('30000')
$selenium.Type('id=searchInput', 'selenium')
$selenium.Click('id=searchButton')
$selenium.WaitForPageToLoad('30000')
$selenium.Click('link=Selenium (software)')
$selenium.WaitForPageToLoad('30000')

脚本的其余部分将保持不变。

RC Driver called from Powershell

当然,您可以在 PowerShell ISE 中直接创建脚本,这将节省大量开发时间。

TreeView

要使用最新版本的 Firefox(例如 33),您需要确保加载 特定版本的 Selenium C# 库——对 Nunit 访问 StringAssert 来说,类似的 버전 检查也很重要。

$shared_assemblies = @{
  'WebDriver.dll' = 2.44;
  'WebDriver.Support.dll' = '2.44';
  'nunit.core.dll' = $null;
  'nunit.framework.dll' = '2.6.3';

}

$shared_assemblies.Keys | ForEach-Object {
  $assembly = $_
  $assembly_path = [System.IO.Path]::Combine($shared_assemblies_path,$assembly)
  $assembly_version = [Reflection.AssemblyName]::GetAssemblyName($assembly_path).Version
  $assembly_version_string = ('{0}.{1}' -f $assembly_version.Major,$assembly_version.Minor)
  if ($shared_assemblies[$assembly] -ne $null) {
 
    if (-not ($shared_assemblies[$assembly] -match $assembly_version_string)) {
      Write-Output ('Need {0} {1}, got {2}' -f $assembly,$shared_assemblies[$assembly],$assembly_path)
      Write-Output $assembly_version
      throw ('invalid version :{0}' -f $assembly)
    }
  }

  if ($host.Version.Major -gt 2) {
    Unblock-File -Path $_;
  }
  Write-Debug $_
  Add-Type -Path $_
}
popd

一个非常有潜力增强的方面是处理文件下载对话框或多选项 Internet Explorer 警报弹出窗口。这些纯 Selenium 支持不佳。要么需要在测试框架中捆绑单独的工具,例如 Autoit,要么需要采用许多解决方法——后者有时感觉有点奇怪。

当 Selenium 测试由 PowerShell 执行时,您可以包含一个从 C# 调用 win32 API 的类,并使用 EnumWindowsGetWindowInfoEnumPropsExGetPropGetWindowTextGetWindowTextLengthGetWindowThreadProcessId win32 API 从 user32.dll 通过 [DllImport()] 并加载 Windows.h 中定义的许多必需结构来访问窗口句柄并调用 PostMessageSendMessage 到所需的按钮,或者简单地调用 CloseWindow 到通过标题找到的 Alert / File Download 对话框。后者会导致一个测试失败,但会阻止整个测试套件在浏览器失去鼠标焦点后挂起。这在网络上的几个资源中得到了解释。

并“另存为”对话框通过向其发送 WM_CLOSE Windows 消息来关闭。

通过一点 P/invoke

    [DllImport("user32.dll")]
    public static extern Int32 SendMessage(IntPtr hwnd, UInt32 Msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPStr)] string lParam);

    [return: MarshalAs(UnmanagedType.SysUInt)]
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
    static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    public static string GetText(IntPtr hWnd)
    {
        int length = GetWindowTextLength(hWnd);
        StringBuilder sb = new StringBuilder(length + 1);
        GetWindowText(hWnd, sb, sb.Capacity);
        return sb.ToString();
    }
    private static string GetWindowClassName(IntPtr hWnd)
    {
        int nRet;
        StringBuilder ClassName = new StringBuilder(256);
        nRet = GetClassName(hWnd, ClassName, ClassName.Capacity);
        return (nRet != 0) ? ClassName.ToString() : null;
    }
    public static void SetText(IntPtr hWnd, String text)
    {

        UInt32 WM_SETTEXT = 0x000C;
        StringBuilder sb = new StringBuilder(text);
        int result = SendMessage(hWnd, WM_SETTEXT, (IntPtr)sb.Length, (String)sb.ToString());
    }

您可以找到对话框的元素并在文件名文本框中输入文本,然后发送一个按钮单击以保存为按钮。

    private static bool EnumWindow(IntPtr handle, IntPtr pointer)
    {
        GCHandle gch = GCHandle.FromIntPtr(pointer);
        String window_class_name = GetWindowClassName(handle);
        // Set textbox text - filename to save
        if (string.Compare(window_class_name, "Edit", true, CultureInfo.InvariantCulture) == 0 ) {
            // http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx
            const UInt32 WM_CHAR = 0x0102;
            const UInt32 WM_KEYDOWN = 0x0100;
            const UInt32 WM_KEYUP = 0x0101;
            const UInt32 VK_RETURN = 0x0D;
            SendMessage(handle, WM_CHAR, new IntPtr(WM_KEYDOWN), IntPtr.Zero);
            SetText(handle, @"c:\temp\my random filename");
            Thread.Sleep(1000);
            SendMessage(handle, WM_CHAR, new IntPtr(VK_RETURN), IntPtr.Zero);
        }
        // Click 'Save'
        if (string.Compare(window_class_name, "Button", true, CultureInfo.InvariantCulture) == 0 ) {
            string button_text = GetText(handle);
            if (string.Compare(button_text, "&Save", true, CultureInfo.InvariantCulture) == 0) {
                SetText(handle, "About to click");
                const UInt32 BM_CLICK = 0x00F5;
                Thread.Sleep(1000);
                SendMessage(handle, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
            }
        }

        List<IntPtr> list = gch.Target as List<IntPtr>;
        if (list == null)
            throw new InvalidCastException("cast exception");
        list.Add(handle);
        return true;
    }

Save As manipulation

请注意,如果不发送“Enter”键,Windows Explorer 将忽略后台输入的文本,并以原始位置/名称保存文件。

Save As manipulation

修改后的代码包含在归档文件中。只需进行最少的努力,即可将类集成到 PowerShell 中,但要使示例真正有用,需要更多工作,并且超出了本文档的范围。

另一个有趣的可能场景是,当目标网站托管在 Linux 主机上的 Tomcat 上运行时,但需要运行 Internet Explorer 集成测试。使用以下样板 Perl 代码片段,可以通过 ssh:cygwin、TeamCity、Jenkins 等远程启动 PowerShell 脚本:

use Net::SSH::Perl;
use Data::Dumper;
use constant DEBUG => 0;

our ($HOSTNAME, $USER, $PASSWORD );
my $POWERSHELL_SCRIPT = ...
$HOSTNAME = '192.168.56.102';
$USER = 'cyg_server';
$PASSWORD = 'cyg_server';

# workaround cygwin console IO challenge
my $ssh_command =
"cat /dev/null|\
/cygdrive/c/Windows/system32/WindowsPowerShell/v1.0/powershell.exe \
-ExecutionPolicy Unrestricted -command \"&{ $POWERSHELL_SCRIPT }\"";

print STDERR $ssh_command if (DEBUG) ;

my $ssh = Net::SSH::Perl->new( $HOSTNAME, debug => 0 );
$ssh->login( $USER, $PASSWORD );

my ( $stdout, $stderr, $exitcode ) = $ssh->cmd( $ssh_command, undef );
print STDERR Dumper \[ $stdout, $stderr, $exitcode ];

1;

END

这显然不是 Selenium grid 测试脚本所必需的,但可以用于其他情况。

例如,通过运行以下教科书式的 PowerShell 脚本通过 ssh:

Import-module WebAdministration
$WebSiteAlias = 'Test'
$AppPoolAlias = 'Test'
pushd 'IIS:\Sites\Default Web Site'
$IISPath = "..\$WebSiteAlias"
if (Test-Path $IISPath) {
  Write-Host "Web Site '$WebSiteAlias' exists."
}
$IISPath = "IIS:\AppPools"
cd $IISPath
if (Test-Path ".$AppPoolAlias") {
  Write-Host "Application Pool '$AppPoolAlias' exists."
}

结果将可供调用脚本使用……

Linux invoking Powershell

当业务运行混合 Tomcat / IIS 网站,并且由于某种原因必须从 Linux 机器编排部署时,这非常有用。在这种情况下,将使用更复杂的 PowerShell 代码,例如执行一些应用程序池检查,调用 msdeploy.exe,然后从 Linux 进行业务特定的网站“预热”。

通用 Selenium 自动化

以下 Selenium 自动化脚本片段从某个邮轮供应商网站中选择加勒比蜜月假期邮轮。选择目的地、日期范围和旅行人数的代码非常冗余,仅显示部分内容。完整的可运行脚本可在 zip 文件中找到。

# Select destination
$value1 = 'dest'
$css_selector1 = ('a[data-param={0}]' -f $value1)
try {

  [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
  $wait.PollingInterval = 150
  [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector1)))
  [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
} catch [exception]{
  Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
}

$element1 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
[NUnit.Framework.Assert]::IsTrue(($element1.Text -match 'Select a destination' ))

Write-Output ('Clicking on ' + $element1.Text)
$element1.Click()
Start-Sleep 1

$value2 =  'C'
$css_selector2 = ('a[data-id={0}]' -f $value2)
try {
  [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
  $wait.PollingInterval = 150
  [OpenQA.Selenium.Remote.RemoteWebElement]$element2 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector2)))
  [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))

} catch [exception]{
  Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
}
$element2 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))

Write-Output ('Clicking on ' + $element2.Text)
[OpenQA.Selenium.Interactions.Actions]$actions2 = New-Object OpenQA.Selenium.Interactions.Actions ($selenium)
$actions2.MoveToElement([OpenQA.Selenium.IWebElement]$element2).Build().Perform()
$actions2.Click().Build().Perform()
Start-Sleep 3


$value1 = 'dat'
$css_selector1 = ('a[data-param={0}]' -f $value1)
try {

  [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
  $wait.PollingInterval = 150
  [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector1)))
##
  [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
} catch [exception]{
  Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
}

$element1 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
[NUnit.Framework.Assert]::IsTrue(($element1.Text -match 'Select a date'))

Write-Output ('Clicking on ' + $element1.Text)
$element1.Click()
Start-Sleep 1

$value2 =  '"022015"'
$css_selector2 = ('a[data-id={0}]' -f $value2)
try {
  [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
  $wait.PollingInterval = 150
  [OpenQA.Selenium.Remote.RemoteWebElement]$element2 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector2)))
##
  [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))

} catch [exception]{
  Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
}
$element2 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))
Write-Output ('Clicking on ' + $element2.Text)
[OpenQA.Selenium.Interactions.Actions]$actions2 = New-Object OpenQA.Selenium.Interactions.Actions ($selenium)
$actions2.MoveToElement([OpenQA.Selenium.IWebElement]$element2).Build().Perform()
$actions2.Click().Build().Perform()
Start-Sleep 3

$value1 = 'numGuests'
$css_selector1 = ('a[data-param={0}]' -f $value1)
try {
  [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
  $wait.PollingInterval = 150
  [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector1)))
##
  [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
} catch [exception]{
  Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
}

$element1 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
[NUnit.Framework.Assert]::IsTrue(($element1.Text -match 'How many travelers'))

Write-Output ('Clicking on ' + $element1.Text)
$element1.Click()
Start-Sleep 1

$value2 =  '"2"'
$css_selector2 = ('a[data-id={0}]' -f $value2)
try {
  [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
  $wait.PollingInterval = 150

  [OpenQA.Selenium.Remote.RemoteWebElement]$element2 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector2)))
##
  [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))

} catch [exception]{
  Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
}
$element2 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))
Write-Output ('Clicking on ' + $element2.Text)
[OpenQA.Selenium.Interactions.Actions]$actions2 = New-Object OpenQA.Selenium.Interactions.Actions ($selenium)
$actions2.MoveToElement([OpenQA.Selenium.IWebElement]$element2).Build().Perform()
$actions2.Click().Build().Perform()
Start-Sleep 3

$css_selector1 = 'div.actions > a.search'
try {
  [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
} catch [exception]{
  Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
}

$element1 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
[NUnit.Framework.Assert]::IsTrue(($element1.Text -match 'SEARCH'))
Write-Output ('Clicking on ' + $element1.Text)
$element1.Click()

Start-Sleep 10

try {
  [OpenQA.Selenium.Screenshot]$screenshot =  $selenium.GetScreenshot()
  $guid = [guid]::NewGuid()
  $image_name = ($guid.ToString())
  [string]$image_path = ('{0}\{1}\{2}.{3}' -f (Get-ScriptDirectory),'temp',$image_name,'.jpg')
  $screenshot.SaveAsFile($image_path,[System.Drawing.Imaging.ImageFormat]::Jpeg)

} catch [exception]{
  Write-Output $_.Exception.Message
}
# Cleanup
try {
  $selenium.Quit()
} catch [exception]{
  # Ignore errors if unable to close the browser
}

Selenium

该脚本可以在除 IE 11 以外的任何浏览器中成功重放。以下代码选择浏览器。

param(
  [string]$browser,
  [int]$version
)
...
if ($browser -ne $null -and $browser -ne '') {
  try {
    $connection = (New-Object Net.Sockets.TcpClient)
    $connection.Connect("127.0.0.1",4444)
    $connection.Close()
  } catch {
    Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\hub.cmd"
    Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\node.cmd"
    Start-Sleep -Seconds 10
  }
  Write-Host "Running on ${browser}"
  if ($browser -match 'firefox') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Firefox()

  }
  elseif ($browser -match 'chrome') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Chrome()
  }
  elseif ($browser -match 'ie') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::InternetExplorer()
    if ($version -ne $null -and $version -ne 0) {
      $capability.SetCapability("version", $version.ToString());
    }
  }

  elseif ($browser -match 'safari') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Safari()
  }
  else {
    throw "unknown browser choice:${browser}"
  }
  $uri = [System.Uri]("http://127.0.0.1:4444/wd/hub")
  $selenium = New-Object OpenQA.Selenium.Remote.RemoteWebDriver ($uri,$capability)
} else {
  Write-Host 'Running on phantomjs'
...

执行时,脚本会打印最少的面包屑,指示已执行的操作。

使用 Selenium sendKeys 上传文件

以下示例在 www.freetranslation.com 上翻译文本。页面包含以下片段。

Three button prompt

<div class="gw-upload-action clearfix">
  <div id="upload-button" class="btn"><img class="gw-icon upload" alt="" src="http://d2yxcfsf8zdogl.cloudfront.net/home-php/assets/home/img/pixel.gif"/>
         Choose File(s)                        
        <div class="ajaxupload-wrapper" style="width: 300px; height: 50px;"><input class="ajaxupload-input" type="file" name="file" multiple=""/></div>
    </div>
</div>

脚本将文本写入文件并上传。

[void]$selenium.Manage().timeouts().ImplicitlyWait([System.TimeSpan]::FromSeconds(60))

$base_url = 'http://www.freetranslation.com/'

$text_file = ('{0}\{1}' -f (Get-ScriptDirectory),'testfile.txt')
Write-Output 'good morning driver' | Out-File -FilePath $text_file -Encoding ascii
$selenium.Navigate().GoToUrl($base_url)
$selenium.Manage().Window.Maximize()
$upload_element = $selenium.FindElement([OpenQA.Selenium.By]::ClassName('ajaxupload-input'))
$upload_element.SendKeys($text_file)

然后等待以下元素出现。

Three button prompt

<a href="..." class="gw-download-link">
  <img class="gw-icon download" src="http://d2yxcfsf8zdogl.cloudfront.net/home-php/assets/home/img/pixel.gif"/>
  Download
</a>
[OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
$wait.PollingInterval = 100

[OpenQA.Selenium.Remote.RemoteWebElement]$element1 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::ClassName("gw-download-link")))

[OpenQA.Selenium.Remote.RemoteWebElement]$element2 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector('img.gw-icon')))
$text_url = $element1.getAttribute('href')

并下载结果。

$result = Invoke-WebRequest -Uri $text_url
[NUnit.Framework.Assert]::IsTrue(($result.RawContent -match 'Bonjour pilote'))

并与已知翻译进行验证。

Selenium IDE PowerShell 格式化程序

接下来,您将从管道中排除 C#,并在 Selenium IDE 中直接录制 PowerShell 事务。完全支持自定义格式化;您无需在早期开发阶段打包 xpi

要继续,作者会 fork 其中一个现有存储库,由 David Zwarg 提供,并修改 C# 格式化程序以遵循 PowerShell 语法并进行其他必要的调整。创建格式化程序所需的所有内容只有一个文件。

Three button prompt

需要小心的一点是不要从基于Selenium Remote Control 的插件开始:RC 插件可以开发,但协议已过时,特别是没有无头驱动程序可用。

格式化程序的完整 JavaScript 源代码尚未在此处显示:这是 alpha 质量的设计,并且拉取请求正在等待中。在 IDE 命令、中间 JavaScript 方法原型和最终 C# 方法调用之间进行转换相当痛苦。

源代码可在作者的 github 存储库中找到。

该插件继承自 webdriver.js

if (!this.formatterType) {
  var subScriptLoader = Components.classes['@mozilla.org/moz/jssubscript-loader;1'].getService(Components.interfaces.mozIJSSubScriptLoader);
  subScriptLoader.loadSubScript('chrome://selenium-ide/content/formats/webdriver.js', this);
}

目前它只添加了最少的功能——目前有相当多的格式化程序具有几乎相同的代码。

修改包括在所有方法引用中提供完整的类路径,例如:

WDAPI.Utils.isElementPresent = function(how, what) {
return "IsElementPresent(" + WDAPI.Driver.searchContext(how, what) + ")";
};

变成

WDAPI.Utils.isElementPresent = function(how, what) {
  return '[Selenium.Internal.SeleniumEmulation]::IsElementPresent(' + WDAPI.Driver.searchContext(how, what) + ')';
};

并调整语义,例如:

Equals.prototype.toString = function() {
  return this.e1.toString() + ' == ' + this.e2.toString() ;
}

变成

Equals.prototype.toString = function() {
  return this.e1.toString() + ' -eq ' + this.e2.toString();
};

使用 Nunit.dll 看起来很自然,但是访问 StringAssert 似乎有点问题,因此您可以选择使用 Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll,如前所述。

早期示例中的所有 PowerShell 初始化代码都进入驱动程序类的头部选项。

this.options = {
  receiver: '$selenium',
  base_url: 'http://docs.seleniumhq.org/docs/02_selenium_ide.jsp',
  driver_namespace: "OpenQA.Selenium.Firefox",
  driver_capabilities: "Firefox()",
  showSelenese: 'false',
  indent: '4',
  initialIndents:  '3',
  header:
     'Param (\n'+
     indents(1) + '[switch] $browser\n'+
     ')\n'
  // ...
   '$capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::${driver_capabilities}\n' +
  // ...

  footer:
     '# Cleanup\n' +
     'try {\n' +
     indents(1) + '$selenium.Quit()\n' +
     '} catch [Exception] {\n' +
     indents(1) + '# Ignore errors if unable to close the browser\n' +
          '}\n',
  defaultExtension: 'ps1'
};

关键属性转换为常规格式化程序输入。

this.configForm =
    '<description>Selenium instance name</description>' +
    '<textbox id="options_receiver" />' +
    '<description>WebDriver Capabilities</description>' +
    '<menulist id="options_driver_capabilities"><menupopup>' +
    '<menuitem label="Firefox" value="Firefox()"/>' +
    '<menuitem label="Google Chrome" value="Chrome()"/>' +
    '<menuitem label="Safari" value="Safari()"/>' +
    '<menuitem label="Internet Explorer" value="InternetExplorer()"/>' +
    '</menupopup></menulist>'+
  // ...

Selenium IDE options

在开发后期,您将安排源代码以适合 xpi,并创建 chrome.manifestinstall.rdfformat-loader.xul,例如:

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<overlay id="webdriver_format_loader_overlay"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">
    <script type="application/x-javascript" src="chrome://selenium-ide/content/api.js"/>
    <html:script type="application/javascript">
        var ide_api = new API();
        ide_api.addPlugin("powershell-webdriver-formatter@serguei.kouzmine");
        ide_api.addPluginProvidedFormatter("powershell-webdriver", "Powershell - WebDriver", "chrome://powershell-webdriver-formatter/content/formats/powershell-webdriver.js");
        ide_api.addPluginProvidedFormatter("powershell-remotecontrol", "Powershell - RC", "chrome://powershell-webdriver-formatter/content/formats/powershell-remotecontrol.js");
    </html:script>
</overlay>

这可以通过简单的批处理命令(或等效的 bash 脚本)打包成独立的 Firefox 附加组件。

@echo off
setlocal

pushd %~dp0


set APP_NAME="powershell-webdriver-formatter"
set CHROME_PROVIDERS="content"

set ROOT_DIR=%CD%
set TMP_DIR="build"

REM remove any left-over files from previous build
del /Q %APP_NAME%.xpi
del /S /Q %TMP_DIR%

mkdir %TMP_DIR%\chrome\content

robocopy.exe content %TMP_DIR%\chrome\content /E
robocopy.exe locale %TMP_DIR%\chrome\locale /E
robocopy.exe skin %TMP_DIR%\chrome\skin /E
robocopy.exe defaults %TMP_DIR%\defaults /E
copy install.rdf %TMP_DIR%
copy chrome.manifest.production %TMP_DIR%\chrome.manifest

rem Package the XPI file
cd %TMP_DIR%
echo "Generating %APP_NAME%.xpi..."

PATH=%PATH%;%ProgramFiles%\7-Zip;%ProgramFiles(x86)%\7-Zip

7z.exe a -r -y -tzip ../%APP_NAME%.zip *

cd %ROOT_DIR%
rename %APP_NAME%.zip %APP_NAME%.xpi

endlocal

Thre logo border

要使用格式化程序:

  • 打开 Selenium IDE,录制事务。
  • 从 Options 菜单中选择 Options。
  • 选择“Formats”选项卡。
    • 如果加载了格式化程序 xpi,请填写输入框,或者
    • 单击“Add”按钮。
    • 命名格式。
    • 粘贴并保存 JavaScript 源(丢失输入)。
  • 在“File” -> “Export Test Case As...”中,选择格式。

如果一切顺利,生成的 PowerShell 脚本将无需修改即可立即运行。

例如,在以下片段中,在加载所需的程序集并启动 Selenium 后,通过在加载的页面上下文中执行 JavaScript 代码,在 Google Logo 周围绘制边框。

$selenium.Navigate().GoToUrl('http://www.google.com')
[OpenQA.Selenium.IWebElement] $element = $selenium.FindElement([OpenQA.Selenium.By]::Id("hplogo"))[OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteScript("arguments[0].setAttribute('style', arguments[1]);", $element, "color: yellow; border: 4px solid yellow;")
start-sleep 3
  [OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteScript("arguments[0].setAttribute('style', arguments[1]);", $element, '')

The Logo border

显然,这里的 JavaScript 是唯一重要的部分。牺牲 C# 项目的开销似乎是合适的。

另一个可能的示例将执行 $selenium.Manage().Timeouts().setScriptTimeout[OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteAsyncScript,然后是 $selenium.FindElement,以便在页面上“盖上”构建信息,或者执行检查并将答案存储在动态追加的 div 元素中,并将断言结果传回脚本(进行中)。

小型开发活动,例如标准的 CI 部署后网站“预热”,也可能通过 Selenium IDE 结合 PowerShell 启动更容易,而不是通过编写单独的应用程序。

在 Explorer 任务栏上显示 Selenium 调试消息

以下示例结合了 Hosting And Changing Controls In Other Applications 的代码和一个典型的 Selenium 事务(此事务涉及框架)。一些网站确实对鼠标悬停事件进行了编码。本例在没有附加监视器(例如在 VirtualBox 中)的情况下,浏览器最大化填满屏幕,没有空间来跟踪执行,因此展示了在此情况下调试事务。

来自 Hosting And Changing Controls In Other Applications 的负责向正在运行的窗口添加额外控件的代码在不进行修改的情况下使用,但计划进行一些更改,将源代码与脚本一起保留,而不是编译成程序集。

Add-Type -TypeDefinition @"
namespace System.Windows
{
    class Win32WindowEvents
    {
//...
    public static class WinAPI
    {
//...
        public static class Win32ControlType
        {
            public static string Button = "Button";

//... 
    ///

目标是将 Windows 控件固定到任务栏。

function custom_debug {
  param(
    [System.Management.Automation.PSReference]$local:button_ref,
    [string]$message
  )
  Write-Debug $message
  $local:button = $local:button_ref.Value
  if ($local:button -eq $null) {
    $exlorer_window = [System.Windows.Win32Window]::FromProcessName('explorer')
    # $window.ClassName = Shell_TrayWnd
    $exlorer_window.Title = "A control WINDOW";
    $local:button = New-Object System.Windows.Win32Button
    # NOTE: The position and size are manually set
    $local:button.TopMost = $true
    $local:button.Width = 600
    $local:button.Height = 60
    $x = ($exlorer_window.Position.Right - $local:button.Width)
    $y = -20

    $local:button.Pos_X = $x
    $local:button.Pos_Y = $y

    $local:button.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',7,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
    $exlorer_window.AddControl($local:button)

    $local:button_ref.Value = $local:button
  }
  $local:button.Text = $message
}

The Debug button

此按钮用于显示调试消息和(进行中)暂停脚本的执行。

$shared_assemblies = @(
  'WebDriver.dll',
  'WebDriver.Support.dll',
  'nunit.core.dll',
  'nunit.framework.dll'
)


$shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'

if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
  $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
}

pushd $shared_assemblies_path
$shared_assemblies | ForEach-Object {

  if ($host.Version.Major -gt 2) {
    Unblock-File -Path $_;
  }
  Write-Debug $_
  Add-Type -Path $_
}
popd
[void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')

$DebugPreference = 'Continue'
# Convertfrom-JSON applies To: Windows PowerShell 3.0 and above
[NUnit.Framework.Assert]::IsTrue($host.Version.Major -gt 2)

$hub_host = '127.0.0.1'
$hub_port = '4444'

$uri = [System.Uri](('http://{0}:{1}/wd/hub' -f $hub_host,$hub_port))
[object]$button = $null

custom_debug ([ref]$button) 'Starting firefox'

if ($browser -ne $null -and $browser -ne '') {
  try {
    $connection = (New-Object Net.Sockets.TcpClient)
    $connection.Connect($hub_host,[int]$hub_port)
    $connection.Close()
  } catch {
    Start-Process -FilePath 'C:\Windows\System32\cmd.exe' -ArgumentList 'start cmd.exe /c c:\java\selenium\hub.cmd'
    Start-Process -FilePath 'C:\Windows\System32\cmd.exe' -ArgumentList 'start cmd.exe /c c:\java\selenium\node.cmd'
    Start-Sleep -Seconds 10
  }
  Write-Host "Running on ${browser}"
  if ($browser -match 'firefox') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Firefox()

  }
  elseif ($browser -match 'chrome') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Chrome()
  }
  elseif ($browser -match 'ie') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::InternetExplorer()
  }
  elseif ($browser -match 'safari') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Safari()
  }
  else {
    throw "unknown browser choice:${browser}"
  }
  $selenium = New-Object OpenQA.Selenium.Remote.RemoteWebDriver ($uri,$capability)
} else {
  # this example may not work with phantomjs 
  $phantomjs_executable_folder = "c:\tools\phantomjs"
  Write-Host 'Running on phantomjs'
  $selenium = New-Object OpenQA.Selenium.PhantomJS.PhantomJSDriver ($phantomjs_executable_folder)
  $selenium.Capabilities.SetCapability("ssl-protocol","any")
  $selenium.Capabilities.SetCapability("ignore-ssl-errors",$true)
  $selenium.Capabilities.SetCapability("takesScreenshot",$true)
  $selenium.Capabilities.SetCapability("userAgent","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.7 Safari/534.34")
  $options = New-Object OpenQA.Selenium.PhantomJS.PhantomJSOptions
  $options.AddAdditionalCapability("phantomjs.executable.path",$phantomjs_executable_folder)
}

[void]$selenium.Manage().timeouts().ImplicitlyWait([System.TimeSpan]::FromSeconds(60))

$selenium.url = $base_url = 'http://translation2.paralink.com'
$selenium.Navigate().GoToUrl(($base_url + '/'))

[string]$xpath = "//frame[@id='topfr']"
[object]$top_frame = $null
find_page_element_by_xpath ([ref]$selenium) ([ref]$top_frame) $xpath
$current_frame = $selenium.SwitchTo().Frame($top_frame)

[NUnit.Framework.Assert]::AreEqual($current_frame.url,('{0}/{1}' -f $base_url,'newtop.asp'),$current_frame.url)
Write-Debug ('Switched to {0} {1}' -f $current_frame.url,$xpath)
custom_debug ([ref]$button) ('Switched to {0} {1}' -f $current_frame.url,$xpath)
$top_frame = $null

[string]$text = 'Spanish-Russian translation'
$css_selector = 'select#directions > option[value="es/ru"]'
[OpenQA.Selenium.IWebElement]$element = $null
find_page_element_by_css_selector ([ref]$current_frame) ([ref]$element) $css_selector
[NUnit.Framework.Assert]::AreEqual($text,$element.Text,$element.Text)
custom_debug ([ref]$button) ('selected "{0}"' -f $text)
$element.Click()
$element = $null

custom_pause

[string]$xpath2 = "//textarea[@id='source']"

[OpenQA.Selenium.IWebElement]$element = $null
find_page_element_by_xpath ([ref]$current_frame) ([ref]$element) $xpath2
highlight ([ref]$current_frame) ([ref]$element)
[OpenQA.Selenium.Interactions.Actions]$actions = New-Object OpenQA.Selenium.Interactions.Actions ($current_frame)
$actions.MoveToElement([OpenQA.Selenium.IWebElement]$element).Click().Build().Perform()

$text = @"
Yo, Juan Gallo de Andrada, escribano de C?mara del Rey nuestro se?or, de los que residen en su Consejo, certifico y doy fe que, habiendo visto por los se?ores d?l un libro intitulado El ingenioso hidalgo de la Mancha, compuesto por Miguel de Cervantes Saavedra, tasaron cada pliego del dicho libro a tres maraved?s y medio; el cual tiene ochenta y tres pliegos, que al dicho precio monta el dicho libro docientos y noventa maraved?s y medio, en que se ha de vender en papel;.
"@
[void]$element.SendKeys($text)
custom_debug ([ref]$button) ('Entered "{0}"' -f $text.Substring(0,100))
$element = $null

Start-Sleep -Milliseconds 1000
$css_selector = 'img[src*="btn-en-tran.gif"]'

$title = 'Translate'
find_page_element_by_css_selector ([ref]$current_frame) ([ref]$element) $css_selector
[NUnit.Framework.Assert]::AreEqual($title,$element.GetAttribute('title'),$element.GetAttribute('title'))
highlight ([ref]$current_frame) ([ref]$element)
[OpenQA.Selenium.Interactions.Actions]$actions = New-Object OpenQA.Selenium.Interactions.Actions ($current_frame)
$actions.MoveToElement([OpenQA.Selenium.IWebElement]$element).Click().Build().Perform()
custom_debug ([ref]$button) ('Clicked on "{0}"' -f $title)

$element = $null
custom_pause

[void]$selenium.SwitchTo().DefaultContent()

[string]$xpath = "//frame[@id='botfr']"
[object]$bot_frame = $null
find_page_element_by_xpath ([ref]$selenium) ([ref]$bot_frame) $xpath
$current_frame = $selenium.SwitchTo().Frame($bot_frame)
[NUnit.Framework.Assert]::AreEqual($current_frame.url,('{0}/{1}' -f $base_url,'newbot.asp'),$current_frame.url)
custom_debug ([ref]$button) ('Switched to {0}' -f $current_frame.url)
$bot_frame = $null

[string]$xpath2 = "//textarea[@id='target']"

[OpenQA.Selenium.IWebElement]$element = $null
find_page_element_by_xpath ([ref]$current_frame) ([ref]$element) $xpath2
highlight ([ref]$current_frame) ([ref]$element)
$text = $element.Text
custom_debug ([ref]$button) ('Read "{0}"' -f $text.Substring(0,100))
custom_pause

# https://code.google.com/p/selenium/source/browse/java/client/src/org/openqa/selenium/remote/HttpCommandExecutor.java?r=3f4622ced689d2670851b74dac0c556bcae2d0fe
# write-output $frame.PageSource
[void]$selenium.SwitchTo().DefaultContent()

$current_frame = $selenium.SwitchTo().Frame(1)
[NUnit.Framework.Assert]::AreEqual($current_frame.url,('{0}/{1}' -f $base_url,'newbot.asp'),$current_frame.url)

custom_pause

[void]$selenium.SwitchTo().DefaultContent()
$current_frame = $selenium.SwitchTo().Frame(0)
[NUnit.Framework.Assert]::AreEqual($current_frame.url,('{0}/{1}' -f $base_url,'newtop.asp'),$current_frame.url)
custom_debug ([ref]$button) ('Switched to {0}' -f $current_frame.url)
custom_pause

[void]$selenium.SwitchTo().DefaultContent()
Write-Debug ('Switched to {0}' -f $selenium.url)

# Cleanup
cleanup ([ref]$selenium)

$button.Visible = $false

The Transaction

完整源代码可在 zip 文件中找到。

Selenium EventFiring WebDriver 示例

以下是 SeleniumEventFiringWebDriver 从 PowerShell 访问的快速示例。通过在 Selenium 事件之后运行代码,可以捕获 Ajax 自动建议的结果。

param(
  [string]$browser = 'firefox',
  [int]$event_delay = 250,
  [switch]$pause

)


function netstat_check
{
  param(
    [string]$selenium_http_port = 4444
  )

  $results = Invoke-Expression -Command "netsh interface ipv4 show tcpconnections"

  $t = $results -split "`r`n" | Where-Object { ($_ -match "\s$selenium_http_port\s") }
  (($t -ne '') -and $t -ne $null)

}

function cleanup
{
  param(
    [System.Management.Automation.PSReference]$selenium_ref
  )
  try {
    $selenium_ref.Value.Quit()
  } catch [exception]{
    Write-Output (($_.Exception.Message) -split "`n")[0]
    # Ignore errors if unable to close the browser
  }
}

$shared_assemblies = @(
  'WebDriver.dll',
  'WebDriver.Support.dll',# for Events
  'nunit.core.dll',
  'nunit.framework.dll'
)

$shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'

if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
  $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
}
pushd $shared_assemblies_path
$shared_assemblies | ForEach-Object {
  # Unblock-File -Path $_; 
  Add-Type -Path $_
}
popd

[void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
$verificationErrors = New-Object System.Text.StringBuilder
$phantomjs_executable_folder = "C:\tools\phantomjs"
if ($browser -ne $null -and $browser -ne '') {
  try {
    $connection = (New-Object Net.Sockets.TcpClient)
    $connection.Connect("127.0.0.1",4444)
    $connection.Close()
  } catch {
    Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\hub.cmd"
    Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\node.cmd"
    Start-Sleep -Seconds 10
  }
  Write-Host "Running on ${browser}" -foreground 'Yellow'
  if ($browser -match 'firefox') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Firefox()

  }
  elseif ($browser -match 'chrome') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Chrome()
  }
  elseif ($browser -match 'ie') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::InternetExplorer()
    if ($version -ne $null -and $version -ne 0) {
      $capability.SetCapability("version",$version.ToString());
    }

  }
  elseif ($browser -match 'safari') {
    $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Safari()
  }
  else {
    throw "unknown browser choice:${browser}"
  }
  $uri = [System.Uri]("http://127.0.0.1:4444/wd/hub")
  $selenium = New-Object OpenQA.Selenium.Remote.RemoteWebDriver ($uri,$capability)
} else {
  Write-Host 'Running on phantomjs' -foreground 'Yellow'
  $phantomjs_executable_folder = "C:\tools\phantomjs"
  $selenium = New-Object OpenQA.Selenium.PhantomJS.PhantomJSDriver ($phantomjs_executable_folder)
  $selenium.Capabilities.SetCapability("ssl-protocol","any")
  $selenium.Capabilities.SetCapability("ignore-ssl-errors",$true)
  $selenium.Capabilities.SetCapability("takesScreenshot",$true)
  $selenium.Capabilities.SetCapability("userAgent","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.7 Safari/534.34")
  $options = New-Object OpenQA.Selenium.PhantomJS.PhantomJSOptions
  $options.AddAdditionalCapability("phantomjs.executable.path",$phantomjs_executable_folder)
}


if ($host.Version.Major -le 2) {
  [void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
  $selenium.Manage().Window.Size = New-Object System.Drawing.Size (600,400)
  $selenium.Manage().Window.Position = New-Object System.Drawing.Point (0,0)
} else {
  $selenium.Manage().Window.Size = @{ 'Height' = 400; 'Width' = 600; }
  $selenium.Manage().Window.Position = @{ 'X' = 0; 'Y' = 0 }
}

$window_position = $selenium.Manage().Window.Position
$window_size = $selenium.Manage().Window.Size

$base_url = 'http://www.google.com/'

# TODO: invoke NLog assembly for quicker logging triggered by the events
# www.codeproject.com/Tips/749612/How-to-NLog-with-VisualStudio

$event = New-Object -Type 'OpenQA.Selenium.Support.Events.EventFiringWebDriver' -ArgumentList @( $selenium)

$element_value_changing_handler = $event.add_ElementValueChanging
$element_value_changing_handler.Invoke(
  {
    param(
      [object]$sender,
      [OpenQA.Selenium.Support.Events.WebElementEventArgs]$eventargs
    )
    Write-Host 'Value Change handler' -foreground 'Yellow'
    if ($eventargs.Element.GetAttribute('id') -eq 'gbqfq') {
      $xpath1 = "//div[@class='sbsb_a']"
      try {
        [OpenQA.Selenium.IWebElement]$local:element = $sender.FindElement([OpenQA.Selenium.By]::XPath($xpath1))
      } catch [exception]{
      }
      Write-Host $local:element.Text -foreground 'Blue'
    }

  })

$verificationErrors = New-Object System.Text.StringBuilder
$base_url = 'http://www.google.com'
$event.Navigate().GoToUrl($base_url)

# protect from blank page
[OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($event,[System.TimeSpan]::FromSeconds(10))
$wait.PollingInterval = 50
[void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::Id("hplogo")))

$xpath = "//input[@id='gbqfq']"

# for mobile
# $xpath = "//input[@id='mib']"

[OpenQA.Selenium.IWebElement]$element = $event.FindElement([OpenQA.Selenium.By]::XPath($xpath))

# http://software-testing-tutorials-automation.blogspot.com/2014/05/how-to-handle-ajax-auto-suggest-drop.html
$element.SendKeys('Sele')
# NOTE:cannot use 
# [OpenQA.Selenium.Interactions.Actions]$actions = New-Object OpenQA.Selenium.Interactions.Actions ($event)
# $actions.SendKeys($element,'Sele')
Start-Sleep -Millisecond $event_delay
$element.SendKeys('nium')
Start-Sleep -Millisecond $event_delay
$element.SendKeys(' webdriver')
Start-Sleep -Millisecond $event_delay
$element.SendKeys(' C#')
Start-Sleep -Millisecond $event_delay
$element.SendKeys(' tutorial')
Start-Sleep -Millisecond $event_delay
$element.SendKeys([OpenQA.Selenium.Keys]::Enter)
Start-Sleep 10

# Cleanup
cleanup ([ref]$event)

The Google suggestions

杂项。实用程序

您可以将 C# 的控制台监视器移植到 PowerShell,以在需要时由某些持续集成构建自动化在网格计算机上定期收集桌面屏幕截图。

# https://codeproject.org.cn/Tips/816113/Console-Monitor
Add-Type -TypeDefinition @"
// "
using System;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Drawing.Imaging;
public class WindowHelper
{
    private int _count = 0;
    public int Count
    {
        get { return _count; }
        set { _count = value; }
    }
   public String TakeScreenshot()
    {
        Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
        Graphics gr = Graphics.FromImage(bmp);
        gr.CopyFromScreen(0, 0, 0, 0, bmp.Size);
        string str = string.Format(@"C:\temp\Snap[{0}].jpeg", _count);
        bmp.Save(str, ImageFormat.Jpeg);
        bmp.Dispose();
        gr.Dispose();
        return str;
    }
    public WindowHelper()
    {
    }

}

"@ -ReferencedAssemblies 'System.Windows.Forms.dll','System.Drawing.dll','System.Data.dll'
$timer = New-Object System.Timers.Timer

[int32]$max_iterations = 20
[int32]$iteration = 0

$action = {

  Write-Host "Iteration # ${iteration}"
  Write-Host "Timer Elapse Event: $(get-date -Format 'HH:mm:ss')"
  $owner = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
  $owner.count = $iteration
  $owner.Screenshot()
  $iteration++
  if ($iteration -ge $max_iterations)
  {
    Write-Host 'Stopping'
    $timer.stop()
    Unregister-Event thetimer -Force 
    Write-Host 'Completed'   
  }
}

Register-ObjectEvent -InputObject $timer -EventName elapsed -SourceIdentifier thetimer -Action $action

请注意,您无法将数据按引用传递给从计时器事件调用的脚本函数,因此您无法远程执行 Add-Type。

 $action = {

  param(
   [System.Management.Automation.PSReference] $ref_screen_grabber 
)
  [Win32Window]$screen_grabber = $ref_screen_grabber.Value

接着

Register-ObjectEvent -InputObject $timer -EventName elapsed -SourceIdentifier thetimer -Action $action -MessageData  ([ref]$owner )

将导致中断。进一步调试此问题正在进行中。

要切换 PowerShell 控制台窗口在显示窗体时最小化,可以使用以下代码:

Add-Type -Name Window -Namespace Console -MemberDefinition @"
// "
[DllImport("Kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
 
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
"@

屏幕截图

您可以将 C# 的控制台监视器移植到 PowerShell,以在需要时由某些持续集成构建自动化在网格计算机上定期收集桌面屏幕截图。

# https://codeproject.org.cn/Tips/816113/Console-Monitor
Add-Type -TypeDefinition @"
// "
using System;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Drawing.Imaging;
public class WindowHelper
{
    private int _count = 0;
    public int Count
    {
        get { return _count; }
        set { _count = value; }
    }
   public String TakeScreenshot()
    {
        Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
        Graphics gr = Graphics.FromImage(bmp);
        gr.CopyFromScreen(0, 0, 0, 0, bmp.Size);
        string str = string.Format(@"C:\temp\Snap[{0}].jpeg", _count);
        bmp.Save(str, ImageFormat.Jpeg);
        bmp.Dispose();
        gr.Dispose();
        return str;
    }
    public WindowHelper()
    {
    }

}

"@ -ReferencedAssemblies 'System.Windows.Forms.dll','System.Drawing.dll','System.Data.dll'
$timer = New-Object System.Timers.Timer

[int32]$max_iterations = 20
[int32]$iteration = 0

$action = {

  Write-Host "Iteration # ${iteration}"
  Write-Host "Timer Elapse Event: $(get-date -Format 'HH:mm:ss')"
  $owner = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
  $owner.count = $iteration
  $owner.Screenshot()
  $iteration++
  if ($iteration -ge $max_iterations)
  {
    Write-Host 'Stopping'
    $timer.stop()
    Unregister-Event thetimer -Force 
    Write-Host 'Completed'   
  }
}

Register-ObjectEvent -InputObject $timer -EventName elapsed -SourceIdentifier thetimer -Action $action

请注意,您无法将数据按引用传递给从计时器事件调用的脚本函数,因此您无法远程执行 Add-Type。

 $action = {

  param(
   [System.Management.Automation.PSReference] $ref_screen_grabber 
)
  [Win32Window]$screen_grabber = $ref_screen_grabber.Value

接着

Register-ObjectEvent -InputObject $timer -EventName elapsed -SourceIdentifier thetimer -Action $action -MessageData  ([ref]$owner )

将导致中断。进一步调试此问题正在进行中。

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form = New-Object System.Windows.Forms.Form
 
$showButton = New-Object System.Windows.Forms.Button
$showButton.Text = 'ShowConsole'
$showButton.Top = 10
$showButton.Left = 10
$showButton.Width = 100
$showButton.add_Click({Show-Console})
$form.controls.Add($showButton)
 
$hideButton = New-Object System.Windows.Forms.Button
$hideButton.Text = 'HideConsole'
$hideButton.Top = 60
$hideButton.Left = 10
$hideButton.Width = 100
$hideButton.add_Click({hide-Console})
$form.controls.Add($hideButton)
 
$Form.ShowDialog()

IpBox

函数操作来自winuser.h 的常量。

function Show-Console {
   $consolePtr = [Console.Window]::GetConsoleWindow()
  #5 show
 [Console.Window]::ShowWindow($consolePtr, 5)
}
 
function Hide-Console {
    $consolePtr = [Console.Window]::GetConsoleWindow()
  #0 hide
 [Console.Window]::ShowWindow($consolePtr, 0)
}

IpBox

在 PowerShell ISE 中编写 Selenium 脚本<</a>

您可能会发现使用 Poweshell ISE 结合 Firebug 或其他浏览器托管的开发人员工具来编写实际脚本很方便。

param(
  [string]$hub_host = '127.0.0.1',
  [string]$browser,
  [string]$version,
  [string]$profile = 'Selenium',
  [switch]$pause = $true
)

function set_timeouts {
  param(
    [System.Management.Automation.PSReference]$selenium_ref,
    [int]$explicit = 120,
    [int]$page_load = 600,
    [int]$script = 3000
  )

  [void]($selenium_ref.Value.Manage().Timeouts().ImplicitlyWait([System.TimeSpan]::FromSeconds($explicit)))
  [void]($selenium_ref.Value.Manage().Timeouts().SetPageLoadTimeout([System.TimeSpan]::FromSeconds($pageload)))
  [void]($selenium_ref.Value.Manage().Timeouts().SetScriptTimeout([System.TimeSpan]::FromSeconds($script)))

}

# http://stackoverflow.com/questions/8343767/how-to-get-the-current-directory-of-the-cmdlet-being-executed
function Get-ScriptDirectory
{
  $Invocation = (Get-Variable MyInvocation -Scope 1).Value
  if ($Invocation.PSScriptRoot) {
    $Invocation.PSScriptRoot
  }
  elseif ($Invocation.MyCommand.Path) {
    Split-Path $Invocation.MyCommand.Path
  } else {
    $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf(""))
  }
}

function cleanup
{
  param(
    [System.Management.Automation.PSReference]$selenium_ref
  )
  try {
    $selenium_ref.Value.Quit()
  } catch [exception]{
    # Ignore errors if unable to close the browser
    Write-Output (($_.Exception.Message) -split "`n")[0]

  }
}

$shared_assemblies = @{
  'WebDriver.dll' = 2.44;
  'WebDriver.Support.dll' = '2.44';
  'nunit.core.dll' = $null;
  'nunit.framework.dll' = '2.6.3';

}

$shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'

if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
  $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
}

pushd $shared_assemblies_path
$shared_assemblies.Keys | ForEach-Object {
  # http://all-things-pure.blogspot.com/2009/09/assembly-version-file-version-product.html
  $assembly = $_
  $assembly_path = [System.IO.Path]::Combine($shared_assemblies_path,$assembly)
  $assembly_version = [Reflection.AssemblyName]::GetAssemblyName($assembly_path).Version
  $assembly_version_string = ('{0}.{1}' -f $assembly_version.Major,$assembly_version.Minor)
  if ($shared_assemblies[$assembly] -ne $null) {
    # http://stackoverflow.com/questions/26999510/selenium-webdriver-2-44-firefox-33
    if (-not ($shared_assemblies[$assembly] -match $assembly_version_string)) {
      Write-Output ('Need {0} {1}, got {2}' -f $assembly,$shared_assemblies[$assembly],$assembly_path)
      Write-Output $assembly_version
      throw ('invalid version :{0}' -f $assembly)
    }
  }

  if ($host.Version.Major -gt 2) {
    Unblock-File -Path $_;
  }
  Write-Debug $_
  Add-Type -Path $_
}
popd

$verificationErrors = New-Object System.Text.StringBuilder

$hub_port = '4444'
$uri = [System.Uri](('http://{0}:{1}/wd/hub' -f $hub_host,$hub_port))

try {
  $connection = (New-Object Net.Sockets.TcpClient)
  $connection.Connect($hub_host,[int]$hub_port)
  $connection.Close()
} catch {
  Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\selenium.cmd"

  Start-Sleep -Seconds 3
}
[object]$profile_manager = New-Object OpenQA.Selenium.Firefox.FirefoxProfileManager

[OpenQA.Selenium.Firefox.FirefoxProfile]$selected_profile_object = $profile_manager.GetProfile($profile)
[OpenQA.Selenium.Firefox.FirefoxProfile]$selected_profile_object = New-Object OpenQA.Selenium.Firefox.FirefoxProfile ($profile)
$selected_profile_object.setPreference('general.useragent.override','Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16')

$selenium = New-Object OpenQA.Selenium.Firefox.FirefoxDriver ($selected_profile_object)
[OpenQA.Selenium.Firefox.FirefoxProfile[]]$profiles = $profile_manager.ExistingProfiles


$DebugPreference = 'Continue'
$base_url = 'https://codeproject.org.cn/'

$selenium.Manage().Window.Size = @{ 'Height' = 600; 'Width' = 480; }
$selenium.Manage().Window.Position = @{ 'X' = 0; 'Y' = 0 }

$selenium.Navigate().GoToUrl($base_url)
set_timeouts ([ref]$selenium)

$css_selector = 'span.member-signin'
Write-Debug ('Trying CSS Selector "{0}"' -f $css_selector)
[OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(1))

try {

  [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector)))

} catch [exception]{
  Write-Output ("Exception with {0}: {1} ...`n(ignored)" -f $id1,(($_.Exception.Message) -split "`n")[0])
}
Write-Debug  ('Found via CSS Selector "{0}"' -f $css_selector )

# highlight the element
[OpenQA.Selenium.IWebElement]$element = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector))
[OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteScript("arguments[0].setAttribute('style', arguments[1]);",$element,'border: 2px solid red;')
Start-Sleep 3
[OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteScript("arguments[0].setAttribute('style', arguments[1]);",$element,'')

# Click on the element:
[OpenQA.Selenium.Interactions.Actions]$actions = New-Object OpenQA.Selenium.Interactions.Actions ($selenium)

try {
$actions.MoveToElement([OpenQA.Selenium.IWebElement]$element).Click().Build().Perform()

} catch [OpenQA.Selenium.WebDriverTimeoutException]{
  # Ignore
  # 
  # Timed out waiting for async script result  (Firefox)
  # asynchronous script timeout: result was not received (Chrome)
  [NUnit.Framework.Assert]::IsTrue($_.Exception.Message -match '(?:Timed out waiting for page load.)')
}

$input_name = 'ctl01$MC$MemberLogOn$CurrentEmail'
  [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(1))
  $wait.PollingInterval = 100

  $xpath = ( "//input[@name='{0}']"  -f  $input_name)
  Write-Debug ('Trying XPath "{0}"' -f $xpath)

try {
  [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementIsVisible([OpenQA.Selenium.By]::XPath($xpath)))
} catch [exception]{
  Write-Output ("Exception with {0}: {1} ...`n(ignored)" -f $id1,(($_.Exception.Message) -split "`n")[0])
}
  Write-Debug ('Found XPath "{0}"' -f $xpath)

[OpenQA.Selenium.IWebElement]$element = $selenium.FindElement([OpenQA.Selenium.By]::XPath($xpath))
[NUnit.Framework.Assert]::IsTrue($element.GetAttribute('type') -match 'email')
$email_str = 'kouzmine_serguei@yahoo.com'
$element.SendKeys($email_str)



# Do not close Browser / Selenium when run from Powershell ISE
if (-not ($host.name -match 'ISE') ) {

if ($PSBoundParameters['pause']) {
  try {
    [void]$host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
  } catch [exception]{}

} else {
  Start-Sleep -Millisecond 1000
}

# Cleanup
 cleanup ([ref]$selenium)
}

让我们剖析这个脚本。以下屏幕截图说明了该过程。

ISE

极端情况

为了举例说明 C# 和 PowerShell 之间相对较大的语法差异,考虑转换处理 IPv4 地址输入字段的自定义输入元素处理程序,摘自 Mervick 的 C# 初学者 IPBox 文章。

C# 版本(片段)

		private void OnTextChange(object sender, System.EventArgs e)
		{
			int box_type = 0;

			CultureInfo MyCultureInfo = new CultureInfo("en-GB");

			double d;

			if( sender.Equals( ip1 ) )
				box_type = 1;
			if( sender.Equals( ip2 ) )
				box_type = 2;
			if( sender.Equals( ip3 ) )
				box_type = 3;
			if( sender.Equals( ip4 ) )
				box_type = 4;

			switch( box_type )
			{
				case 1:

					if( this.ip1.Text.Length > 0 && this.ip1.Text.ToCharArray()[this.ip1.Text.Length - 1] == '.' )
					{
						this.ip1.Text = this.ip1.Text.TrimEnd( '.' );
						ip1.Text = (this.ip1.Text.Length > 0 ) ? int.Parse( this.ip1.Text ).ToString() : "0" ;
						ip2.Focus();
						return;
					}

					// integer validation
					if( double.TryParse(
						this.ip1.Text,
						System.Globalization.NumberStyles.Integer,
						MyCultureInfo,
						out d ) == false
						)
					{
						this.ip1.Text = this.ip1.Text.Remove( 0, this.ip1.Text.Length );
						return;
					}

					// change focus to the next textbox if fully inserted
					if( this.ip1.Text.Length == 3 )
					{
						if( int.Parse( this.ip1.Text ) >= 255 )
							this.ip1.Text = "255";
						else
							ip1.Text = int.Parse( ip1.Text ).ToString();
						ip2.Focus();
					}
					break;
				case 2:
...

等效的 PowerShell 版本。

function text_changed () {
param(
[object]$sender,
[System.EventArgs]$eventargs
)
[int]$box_type = 0
[System.Globalization.CultureInfo]$ci = New-Object System.Globalization.CultureInfo ("en-GB")
[double]$d = 0
if ($sender -eq $ip1) {
$box_type = 1 }
if ($sender -eq $ip2) {
$box_type = 2 }
if ($sender -eq $ip3) {
$box_type = 3 }
if ($sender -eq $ip4) {
$box_type = 4 }
switch ($box_type)
{
1 {
if (($ip1.Text.Length -gt 0) -and ($ip1.Text.ToCharArray()[$ip1.Text.Length - 1] -eq '.'))
{
$ip1.Text = $ip1.Text.TrimEnd('.')
if ($ip1.Text.Length -gt 0) {
$ip1.Text = [int]::Parse($ip1.Text).ToString()
} else {
$ip1.Text = '0'
}
$ip2.Focus()
return
}
# integer validation
if ([double]::TryParse(
$ip1.Text,
[System.Globalization.NumberStyles]::Integer,
$ci,
([ref]$d)) -eq $false
)
{
$ip1.Text = $ip1.Text.Remove(0,$ip1.Text.Length)
return
}
# change focus to the next textbox if fully inserted
if ($ip1.Text.Length -eq 3) {
if ([int]::Parse($ip1.Text) -ge 255) {
$ip1.Text = '255'
} else {
$ip1.Text = [int]::Parse($ip1.Text).ToString()
}
$ip2.Focus()
}
}
2 {
...

IpBox

在此示例中,可能应该避免转换。完整的脚本源代码可在源 zip 文件中找到。

剖析过程

初步讨论

在本节中,我们将 C# 代码分步转换为可运行的 PowerShell 脚本,分 3 步,然后是另外 2 步。

  • http://www.java2s.com/Code/CSharp/GUI-Windows-Form/MyClockForm.htm 下载代码,将其保存为文本文件 timer.cs。编译并确保其在控制台中运行。

    C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe timer.cs
    
    invoke-expression -command './timer.exe'
  • 创建一个空白文本文件 timer_iter1.ps1,并在其中放入以下样板代码:

    Add-Type -TypeDefinition @"
    // -- about to paste the c# code below. Any class would do
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll', 'System.Drawing.dll', 'System.Data.dll', 'System.ComponentModel.dll'
    
    $clock = New-Object MyClock.MyClockForm
    $clock.ShowDialog()
    $clock.Dispose()

    检查要转换的类的命名空间和类名,确保 PowerShell 创建的是同一类的实例。

    namespace MyClock
    {
    public class MyClockForm : System.Windows.Forms.Form {
    /// implementation
    } }

    因此 New-Object MyClock.MyClockForm

    找出 C# 类“using”区域中所需的程序集。

    using System;
    using System.Drawing;
    using System.Collections;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.Data;

    将类的代码粘贴到 PowerShell 脚本 Add-Type cmdlet TypeDefinition 的文本参数中,并确保其可运行。

    . ./timer_iter1.ps1
  • 如果收到错误

    Add-Type : Cannot add type. The type name 'Win32Window' already exists.

    PowerShell 窗口需要重启。当然,如果您收到

    Add-Type : Cannot add type. Compilation errors occurred.
    FullyQualifiedErrorId : SOURCE_CODE_ERROR,

    您需要修复代码。

    PowerShell 版本的类应该看起来和感觉与编译后的可执行文件一样,但显然还没有在脚本和对话框之间共享数据的明显方法。

  • 现在将脚本过程显式地转变为对话框的caller

    请注意,http://msdn.microsoft.com/en-us/library/system.windows.forms.form.showdialog(v=vs.90).aspx 描述了 ShowDialog 方法的两个备用签名,每个 Windows 窗体都响应。后者接受所有者对象。

    ShowDialog(IWin32Window) 将窗体显示为具有指定调用者的模态对话框。

    任何实现 IWin32Window 的类都可以成为任意 Windows 窗体内部的模态对话框的所有者。

    因此,我们使用一个纯 C# 对象代码源重复之前的 Add-Type 代码混合练习。

    Add-Type -TypeDefinition @"
    
    // "
    using System;
    using System.Windows.Forms;
    public class Win32Window : IWin32Window
    {
        private IntPtr _hWnd;
        private int _data;
        private string _message;
    
        public int Data
        {
            get { return _data; }
            set { _data = value; }
        }
        public string Message
        {
            get { return _message; }
            set { _message = value; }
        }
    
        public Win32Window(IntPtr handle)
        {
            _hWnd = handle;
        }
    
        public IntPtr Handle
        {
            get { return _hWnd; }
        }
    }
    
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll'

    上面的代码实现了接口IWin32Window 所需的单个方法——带窗口句柄的构造函数。上面代码中的其他属性 DataMessage 不是接口所必需的,但对于将各部分连接在一起至关重要。

  • 最后,修改代码以处理调用者。

    • 将参数传递给 Windows.Forms
      $process_window = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle )
      
        $timer.ShowDialog([Win32Window ] ($process_window) ) | out-null
      
      write-output $process_window.GetHashCode()
    • 从窗体内部访问对象。

      您需要为该类添加一个成员变量,并修改以下两个方法。请注意,在实现 PowerShell 版本时不需要这样做。一定有更好的方法来演示这一点。现在,目标是迁移到 PowerShell 版本,并最终丢弃修改后的类。这种“理由”上的 hack。

      private void OnTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
              {
                  // The interval has elapsed and this timer function is called after 1 second
                  // Update the time now.
                  label1.Text = DateTime.Now.ToString();
      
                  label1.Text = String.Format("My Clock {0} {1}", caller.ToString(), caller.GetHashCode() );
              }
      
          public new DialogResult ShowDialog(IWin32Window caller){
              this.caller = caller ;
              return base.ShowDialog(caller);
          }

      另一方面,当要移植的代码比此示例更复杂的窗体时,通过同一个对象 $caller 交换所有特定领域的数据将会很有帮助,无论其复杂性如何。您可以 Visual Studio 或 PowerShell ISE 中测试管道的任何一侧,并模拟另一侧,而不必过多担心细节。

    将代码另存为 timer_iter2.ps1 并确认它仍然运行。

    运行脚本会产生相同的对象,该对象可供脚本和窗体使用。

    Dialog result

实际转换为 PowerShell

下一步是选择性地重写窗体的方法和元素,并移除“混合”代码。让 C# 编译器接受 $caller 响应许多额外数据消息的事实并不容易。另一种选择是使用反射,但不会产生简洁或漂亮的 PING。

所需代码编辑都是语义上的。

  • 移除实例引用(this)以及类声明、构造函数、命名空间等。成员 this.timer1 变成 $timer1,依此类推。this 变成简单的 $f——窗体对象。
  • 修改方法调用的语义:new System.Timers.Timer(); 变成 new-object System.Timers.Timer,等等。当在方法调用参数中找到类实例化时,似乎可以将嵌套方法调用分开。
  • 更改常量解析的语义:System.Drawing.ContentAlignment.MiddleCenter 将变为 [System.Drawing.ContentAlignment]::MiddleCenter 等。始终提供完全解析的类名:ImageList il = new ImageList(); 将必须变为 $il = new-object System.Windows.Forms.ImageList 等。如果不确定,请查阅 MSDN。
  • 注意细微的语义差异,例如 -eq 代替 ==-bor 代替 | 等。
  • 首先运行可视化布局,但注释掉事件传播。一旦窗体开始显示,再处理事件。

    确保事件处理程序*在*使用它们之前定义好:例如,将以下代码的前几行移到顶部

    $button1_Click = {
       param(
        [Object] $sender,
        [System.EventArgs] $eventargs
        )
        [System.Windows.Forms.MessageBox]::Show('hello');
    }
    $button1.Add_Click($button1_Click)

    这样,当 $button1 被点击时,窗体将不再显示空白的 messagebox

  • 创建一个包装的 PowerShell 函数,并添加代码来使窗体可见。
    $f.ResumeLayout($false)
    $f.Topmost = $true
    $f.Activate()
    $f.Displose()

    $callershowDialog(...) 移到 PowerShell 函数内部。

    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    [void] $f.ShowDialog([Win32Window ] ($caller) )

结果将如下所示

function exampleTimer(
    [Object] $caller= $null
    )
{
$f = New-Object System.Windows.Forms.Form
$f.Text = $title

$timer1 = new-object System.Timers.Timer
$label1 = new-object System.Windows.Forms.Label


$f.SuspendLayout()
$components = new-object System.ComponentModel.Container
$label1.Font = new-object System.Drawing.Font("Microsoft Sans Serif", 14.25, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);
$label1.ForeColor = [System.Drawing.SystemColors]::Highlight
$label1.Location = new-object System.Drawing.Point(24, 8)
$label1.Name = "label1"
$label1.Size = new-object System.Drawing.Size(224, 48)
$label1.TabIndex = 0;
$label1.Text = [System.DateTime]::Now.ToString()
$label1.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter

$f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
$f.ClientSize = new-object System.Drawing.Size(292, 69)
$f.Controls.AddRange(@( $label1))
$f.Name = 'MyClockForm';
$f.Text = 'My Clock';

# This was added - it does not belong to the original Form
$eventMethod=$label1.add_click
$eventMethod.Invoke({$f.Text="You clicked my label $((Get-Date).ToString('G'))"})

# This silently ceases to work
$f.Add_Load({
  param ([Object] $sender, [System.EventArgs] $eventArgs )
    $timer1.Interval = 1000
    $timer1.Start()
    $timer1.Enabled = $true

})

$timer1.Add_Elapsed({
     $label1.Text = [System.DateTime]::Now.ToString()
})

# This loudly ceases to start the timer "theTimer"
$global:timer = New-Object System.Timers.Timer
$global:timer.Interval = 1000
Register-ObjectEvent -InputObject $global:timer -EventName Elapsed -SourceIdentifier theTimer -Action {AddToLog('') }
$global:timer.Start()
$global:timer.Enabled = $true

function AddToLog()
{
param ([string] $text )

     $label1.Text = [System.DateTime]::Now.ToString()
}

$f.ResumeLayout($false)
$f.Topmost = $True

if ($caller -eq $null ){
  $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
}

$f.Add_Shown( { $f.Activate() } )
$f.ShowDialog([Win32Window] ($caller) )

}

这样几乎所有东西都就绪了,除了事件处理程序似乎没有被触发——时间戳未更新。这段代码显然需要修复。

调试 Timer 问题

经过一些调试,发现脚本没有正确处理 Windows.Form 类实例曾经拥有但现在不再拥有的 Timer 对象。这是一个需要修复的独立问题,目前正在处理中。为了证明*大多数*事件处理程序*几乎可以*毫不费力地转换为运行 PowerShell 代码,我们向 label 添加了 click 处理程序。

$eventMethod=$label1.add_click
$eventMethod.Invoke({$f.Text="You clicked my label $((Get-Date).ToString('G'))"})

PromptForChoice

并进行了点击。结果符合预期。

总结一下,根据 C# 蓝图编写等效的 PowerShell 代码来布局窗体以及处理事件,是本章前面承诺的最后两个步骤。

可视化设计复制步骤显然是轻而易举的,最多只是一个打字练习。有了 Windows Presentation Foundation 甚至更简单:可以直接加载相同的 XAML。

相反,事件管理可能需要一些精力来掌握。

在本文档的 PowerShell 示例中,每次都尝试了略有不同的事件处理代码语义。这种多样性是故意引入的——所有变体都是等效的——.NET Framework 在后台生成大量代码来支持 MulticastDelegate

总结一下,根据 C# 蓝图在 PowerShell 中复制可视化设计以及处理事件,是本章前面承诺的最后两个步骤。可视化设计步骤轻而易举,最多只是一个打字练习。相反,事件管理可能需要一些精力来掌握。在本文档的 PowerShell 示例中,每次都选择了略有不同的事件处理代码语义。这种多样性是故意引入的——所有变体都是等效的。在后台,MS .NET 会生成大量代码来继承 MulticastDelegate

PromptForChoice

PowerShell 内置的提示机制主要用于控制破坏性操作。其确切的呈现方式取决于运行 PowerShell 脚本的主机。在 http://technet.microsoft.com/en-us/library/ff730939.aspx 中为基本的“是?否?也许”多项选择建议的无限循环解决方案几乎是不可接受的。它传递了一个明确的信息:“放弃多选提示”。

$heads = New-Object System.Management.Automation.Host.ChoiceDescription "&Heads", "Select Heads."
    $tails = New-Object System.Management.Automation.Host.ChoiceDescription "&Tails", "Select Tails."
    $cancel = New-Object System.Management.Automation.Host.ChoiceDescription "&Cancel", "Skip to the next step."
    $options = [System.Management.Automation.Host.ChoiceDescription[]]($heads, $tails, $cancel)
    $host.ui.PromptForChoice("Call it","----", $options,2 )

PromptForChoice

它根据 ConsoleHostWindows PowerShell ISE Host 中的*主机*功能呈现不同的效果。

PromptForChoice

并返回所选选项的索引 - 0, 1, 2。

平台兼容性

本文档中的 PowerShell 脚本已在以下平台验证通过:

Windows Server 2012 - 桌面体验
Windows Server 2012 - 最小服务器界面,Windows Server 2012 - Windows Server Core 大多数示例都能正常工作,但有一个例外:toggle_display.ps1 可以显示窗体,然后隐藏,但从未能重新显示 PowerShell 控制台。
Windows Server 2008 R2
Windows Server 2008
Windows Server 2003
Windows 8 ?
Windows 7
Windows Vista
Windows XP
Windows 2000

历史

最初的工作是自动化日常的 DevOps 例程,配置包含 Microsoft 软件的标准化 UAT 环境,托管在私有云中。其中一个特别繁琐的步骤是通过 SQL Server 客户端网络配置实用程序选择性地克隆 SQL 配置。后者非常不用户友好。

cliconfg.exe

在后台,所有信息都存储在一个注册表项中。这使得从远程主机加载这些信息成为自动化的良好候选,但操作员的角色仍然至关重要,因为环境景观之间存在细微差别:哪些 IIS 应用程序托管在哪台计算机上。如果设置被转换为类似 Puppet 的节点定义,这本不是问题。

GitHub 上的源代码

对于大多数示例,完整的源代码在本文章和附带的 zip 文件中提供。也可以从 Github 克隆完成的源代码。

发布历史

  • 2014-07-21 - 初始版本
  • 2014-07-21 - 添加了更多示例
  • 2014-07-22 - 添加了关于代码转换的注释
  • 2014-07-22 - 添加了 XAML 示例
  • 2014-07-23 - 添加了 TreeView 示例
  • 2014-07-24 - 添加了 Dissect Conversion 示例
  • 2014-07-25 - 添加了带有 Treeview 的自定义图标
  • 2014-07-25 - 添加了关于 Get-Credential cmdlet 的说明
  • 2014-07-26 - 添加了 TabControl 和 Focus 示例
  • 2014-07-26 - 添加了目录
  • 2014-07-26 - 添加了 Tabbed Treeviews
  • 2014-07-26 - 重构了示例代码片段
  • 2014-07-27 - 添加了 WebBrowser1 示例
  • 2014-07-27 - 添加了平台兼容性矩阵
  • 2014-07-28 - 添加了即时生成 XAML 对话框的示例
  • 2014-07-29 - 添加了脚本参数提示 DataGridView 示例
  • 2014-07-29 - 添加了填充颜色和 ZIndex 操作示例
  • 2014-07-29 - 添加了 WPF Form 文本操作示例
  • 2014-07-29 - 添加了双向 Form Script 文本通信示例
  • 2014-08-09 - 添加了 Selenium Script 示例
  • 2014-08-09 - 修改了 Selenium Grid Test 示例以在 Safari 浏览器上执行
  • 2014-08-09 - 添加了关于文件下载对话框处理的说明
  • 2014-08-10 - 添加了带有 ComboBox 的 TreeView 控件示例
  • 2014-08-10 - 添加了代码格式化缺陷的解决方法
  • 2014-08-11 - 添加了 ProgressBar 示例
  • 2014-08-13 - 添加了 Selenium IE 对话框处理器示例
  • 2014-08-13 - 修复了格式并分离了一些内联 XAML 代码以提高可读性
  • 2014-08-16 - 添加了 Selenium IDE Powershell Formatter 示例
  • 2014-08-16 - 更新了指向作者 Powershell Selenium IDE Formatter git 仓库的链接
  • 2014-08-19 - 添加了拖放示例
  • 2014-08-22 - 添加了通过 Selenium 运行 Javascript 的示例
  • 2014-08-22 - 添加了 Microsoft Test Agent DLL 发现示例
  • 2014-08-22 - 添加了 xpi 的概述和构建说明
  • 2014-08-23 - 添加了点击“保存”对话框按钮的示例
  • 2014-08-23 - 添加了从 Linux 运行 Powershell 的示例
  • 2014-08-24 - 更新了“保存”对话框示例版本,以接受指定的下载文件路径
  • 2014-09-03 - 添加了 WebDriver 拖放示例
  • 2014-09-09 - 添加了杂项 WebDriver 示例
  • 2014-09-09 - 添加了隐藏 Powershell 控制台窗口的示例
  • 2014-09-09 - 添加了关于 Windows Server Core 中 Powershell UI 的说明
  • 2014-09-21 - 添加了柱状图 (VB.Net) 示例
  • 2014-09-24 - 添加了上下选择器示例
  • 2014-09-26 - 添加了超时确认对话框示例
  • 2014-10-07 - 添加了极端情况示例,恢复了几个损坏的段落,执行了少量 HTML 格式清理
  • 2014-10-07 - 添加了 Selenium SendKeys 示例
  • 2014-10-07 - 恢复了 Selenium IDE Powershell Formatter 部分
  • 2014-10-07 - 恢复了 DropDown ComboBox 部分
  • 2014-11-01 - 添加了文件系统 Treeview 示例
  • 2014-11-03 - 更新了包含最终文件系统 Treeview 和自定义 MsgBox 示例的 Source Zip
  • 2014-11-04 - 添加了自定义 MsgBox 示例
  • 2014-11-14 - 添加了 Ribbon 示例
  • 2014-11-14 - 添加了 Selenium Powershell ISE 示例
  • 2014-12-07 - 添加了可折叠列表示例
  • 2014-12-14 - 添加了已选组合列表框示例
  • 2014-12-20 - 添加了饼图和柱状图绘制示例
  • 2014-12-22 - 添加了 Timer 示例
  • 2015-01-04 - 添加了任务列表进度示例
  • 2015-01-05 - 评论了任务列表进度
  • 2015-01-14 - 添加了手风琴菜单示例
  • 2015-01-14 - 添加了手风琴菜单代码重构示例
  • 2015-01-17 - 添加了圆形进度指示器示例
  • 2015-01-19 - 添加了圆形进度指示器 W2K3 兼容性补丁
  • 2015-02-07 - 重构了 Ribbon 按钮示例
  • 2015-02-15 - 添加了 Selenium 调试消息到 Explorer 任务栏的示例
  • 2015-02-16 - 添加了 Selenium EventFiring WebDriver 示例 *WIP
  • 2015-02-17 - 修复了格式缺陷
  • 2015-02-27 - 添加了 TreeTabControl 示例
  • 2015-02-27 - 继续 TreeTabControl 示例 *WIP
  • 2015-03-10 - 添加了替代的 Add-Type 语法示例。删除了空行。
  • 2015-03-22 - 提供了替代的 $script: 语法示例并上传了拼写修复。
  • 2015-03-23 - 添加了关于 System.Management.Automation.TypeAccelerators 的说明。
  • 2015-03-25 - 添加了测试配置显示示例。
  • 2015-04-04 - 替换并简化了自定义调试消息框示例。
  • 2015-04-05 - 添加了 OS X 圆形进度指示器示例。
  • 2015-04-10 - 添加了可排序的 ListView 示例。
  • 2015-04-17 - 添加了填充 GridView 示例。
  • 2015-05-31 - 添加了通用对话框示例。
  • 2015-05-3 - 添加了通用对话框示例。

    目录

    引言

    PowerShell 是一个高级脚本框架,通常脚本在控制台主机中运行,最常见的是远程运行,但 PowerShell 脚本在 Windows 计算机上仍然相对频繁地用于交互式操作。当一个通用脚本执行时,它很可能需要选择一个以上的选项。需要以级联的方式向用户提供多个选项,并且通常需要复杂的选择场景。对于某些数据选择,GUI 比 CLI 更直观、更快——在控制台中,即使是基本选择看起来也不太好。

    在许多情况下,老式的 Windows Forms 仍然是提示用户的一种便捷方式。这是本文的主要重点。我们检查了来自 http://www.java2s.com/ 的几个基本示例,并将它们转换为 PowerShell。稍后,我们将使用早期的示例作为构建更复杂事物的基石。所有这些示例的代码都包含在一个文件中,并且不需要合并单独的设计师代码,这大大简化了转换。重点在于保持新兴的 PowerShell 代码尽可能少,以处理各种数据选择场景,包括提示、密码、复选框、单选按钮、选中列表、网格、树视图、制表对话框以及它们的组合。此外,还将演示窗体元素特定的事件处理程序将执行 PowerShell 代码。最后,像 TreeView 这样的控件本身就能很好地可视化数据,并可能使几次提示变得不必要。

    另一方面,Windows Presentation Foundation 可能会感觉有点沉重难以启动和/或调试,但完全可行——本文档中间提供了示例。与 WPF 交互需要多线程,而此技术对于长运行脚本的异步状态报告也很有价值。

    一个令人欣慰的注意点是,所有脚本在最小服务器界面(Minimal Server Interface)甚至服务器核心(Server Core)Windows Server 2012 GUI levels 下仍然有效。原因如下:即使在“服务器图形 shell”(Server Graphical Shell)和“服务器图形管理工具和基础架构”(Server Graphical Management Tools & Infrastructure)Windows 功能被“移除”之后,完整的 Microsoft .Net Framework 仍然存在。示例的最终目标是为复杂自定义数据提供熟悉的 UI——这仍然可以在 Windows Server Core 上实现。请注意,由于即使在 Server Core 中也可用鼠标,因此不需要为窗体元素添加键盘快捷键。

    在后续示例中,将展示如何手动将 PowerShell Selenium 脚本从 C# 等效脚本构建,或自动在 Selenium IDE 中录制;并说明使用 PowerShell 运行 Selenium 录制的明显好处。

    最后,详细介绍了分步转换过程。

    背景

    人们会发现 PowerShell 版本代码与 C# 版本几乎相同,只有语义差异。所有源代码均可在作者的 github 存储库中找到,并且每天都在开发新代码。

    目前,我们需要构建一个辅助类,负责将信息传递给 PowerShell 脚本调用者,使用纯 C# 编写,并使其属性在事件处理程序中对 Windows 窗体可用,尽管所有对话框都将以模态方式绘制。如果没有这种紧密的联系,可能会出现一些难以调试的竞争条件错误。这些假设的分析被推迟到未来的文章。

    Using the Code

    本文档提供的示例希望读者能够轻松地根据自己的需求进行定制。

    代码详情

    将用于在窗体和 PowerShell 之间共享信息的类非常基础。它只需要实现 IWin32Window 接口;它还将具有各种 private 数据成员,以及用于在下面一些示例的窗体中使用的 getter、setter 和方法。

    Add-Type -TypeDefinition @"
    
    // "
    using System;
    using System.Windows.Forms;
    public class Win32Window : IWin32Window
    {
        private IntPtr _hWnd;
        private int _data;
    
        public int Data
        {
            get { return _data; }
            set { _data = value; }
        }
    
        public Win32Window(IntPtr handle)
        {
            _hWnd = handle;
        }
    
        public IntPtr Handle
        {
            get { return _hWnd; }
        }
    }
    
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll'

    PowerShell 将其自己的窗口句柄存储在类中

    if ($process_window -eq $null ){
      $process_window = New-Object Win32Window -ArgumentList
      ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    }

    条目选择和整体状态从 $caller.Message$caller.Data 读取

    $DebugPreference = 'Continue'
    
      if($process_window.Data -ne $RESULT_CANCEL) {
        write-debug ('Selection is : {0}' -f  , $process_window.Message )
      } else {
        write-debug ('Result is : {0} ({1})' -f
        $Readable.Item($process_window.Data) , $process_window.Data )
      }

    备用语法可以是

    $guid = [guid]::NewGuid()
    
    $helper_namespace = ("Util_{0}" -f ($guid -replace '-',''))
    $helper_name = 'Helper'
    
    Add-Type -UsingNamespace @(
      'System.Drawing',
      'System.IO',
      'System.Windows.Forms',
      'System.Drawing.Imaging',
      'System.Collections.Generic',
      'System.Text' `
      ) `
       -MemberDefinition @"
    // inline C# code without class decoration
    "@ -ReferencedAssemblies @( 'System.Windows.Forms.dll',`
         'System.Drawing.dll',`
         'System.Data.dll',`
         'System.Xml.dll') `
       -Namespace $helper_namespace -Name $helper_name -ErrorAction Stop
    
    $helper = New-Object -TypeName ('{0}.{1}' -f $helper_namespace,$helper_type)
    # the rest of Powershell code 
    

    这样,每次修改内联 C# 代码时,您就不必担心看到烦人的警告了

    Add-Type : Cannot add type. The type name 'Win32Window' already exists.
    At C:\developer\sergueik\powershell_ui_samples\treeview_c.ps1:21 char:1
    + Add-Type -TypeDefinition @"
    

    请注意,一些命名空间已经默认包含,不应显式提供在调用参数中,以避免

     Warning as Error: 
     The using directive for 'System' appeared previously in this namespace
     The using directive for 'System.Runtime.InteropServices' appeared previously in this namespace
    

    多项选择提示

    Three button prompt

    多项选择决策提示是最简单的示例,它不需要窗体元素之间进行任何通信——窗体在每个按钮的 Click 事件处理程序中独立设置 $caller.Data

    function PromptAuto(
        [String] $title,
        [String] $message,
        [Object] $caller = $null
        ){
    
    [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
    [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
    
    $f = New-Object System.Windows.Forms.Form
    $f.Text = $title
    
    $f.Size = New-Object System.Drawing.Size(650,120)
    $f.StartPosition = 'CenterScreen'
    
    $f.KeyPreview = $True
    $f.Add_KeyDown({
    
        if     ($_.KeyCode -eq 'Y')       { $caller.Data = $RESULT_POSITIVE }
        elseif ($_.KeyCode -eq 'N')       { $caller.Data = $RESULT_NEGATIVE }
        elseif ($_.KeyCode -eq 'Escape')  { $caller.Data = $RESULT_CANCEL }
        else                              { return }
        $f.Close()
    
    })
    
    $b1 = New-Object System.Windows.Forms.Button
    $b1.Location = New-Object System.Drawing.Size(50,40)
    $b1.Size = New-Object System.Drawing.Size(75,23)
    $b1.Text = 'Yes!'
    $b1.Add_Click({ $caller.Data = $RESULT_POSITIVE; $f.Close(); })
    
    $b2 = New-Object System.Windows.Forms.Button
    $b2.Location = New-Object System.Drawing.Size(125,40)
    $b2.Size = New-Object System.Drawing.Size(75,23)
    $b2.Text = 'No!'
    $b2.Add_Click({ $caller.Data = $RESULT_NEGATIVE; $f.Close(); })
    
    $b3 = New-Object System.Windows.Forms.Button
    $b3.Location = New-Object System.Drawing.Size(200,40)
    $b3.Size = New-Object System.Drawing.Size(75,23)
    $b3.Text = 'Maybe'
    $b3.Add_Click({$caller.Data =  $RESULT_CANCEL ; $f.Close()})
    
    $l = New-Object System.Windows.Forms.Label
    $l.Location = New-Object System.Drawing.Size(10,20)
    $l.Size = New-Object System.Drawing.Size(280,20)
    $l.Text = $message
    
    $f.Controls.Add($b1)
    $f.Controls.Add($b3)
    $f.Controls.Add($b2)
    $f.Controls.Add($l)
    $f.Topmost = $True
    
    if ($caller -eq $null ){
      $caller = New-Object Win32Window -ArgumentList
      ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    }
    
    $caller.Data = $RESULT_CANCEL;
    $f.Add_Shown( { $f.Activate() } )
    
    [void] $f.ShowDialog([Win32Window ] ($caller) )
    $f.Dispose()
    }

    选项文本和定义在函数中硬编码。

    $RESULT_POSITIVE = 0
    $RESULT_NEGATIVE = 1
    $RESULT_CANCEL = 2
    
    $Readable = @{
        $RESULT_NEGATIVE = 'NO!';
        $RESULT_POSITIVE = 'YES!' ;
        $RESULT_CANCEL = 'MAYBE...'
        }
    
    $process_window = New-Object Win32Window -ArgumentList
    ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    
    $title = 'Question'
    $message =  "Continue to Next step?"
    
    $result = PromptAuto -title  $title -message $message  -caller $process_window
    
    write-debug ("Result is : {0} ({1})" -f $Readable.Item($process_window.Data) , $process_window.Data )

    超时提示

    关闭空闲输入框的流行功能可以通过例如添加一个包含 System.Timers.TimerSystem.Windows.Forms.Panel 子类来提供

    using System;
    using System.Drawing;
    using System.Windows.Forms;
    
    public class TimerPanel : System.Windows.Forms.Panel
    {
        private System.Timers.Timer _timer;
        private System.ComponentModel.Container components = null;
        public System.Timers.Timer Timer
        {
            get
            {
                return _timer;
            }
            set { _timer = value; }
        }
    
        public TimerPanel()
        {
            InitializeComponent();
        }
    
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (components != null)
                {
                    components.Dispose();
                }
            }
            _timer.Stop();
            base.Dispose(disposing);
        }
    
        private void InitializeComponent()
        {
            this._timer = new System.Timers.Timer();
            ((System.ComponentModel.ISupportInitialize)(this._timer)).BeginInit();
            this.SuspendLayout();
            this._timer.Interval = 1000;
            this._timer.Start();
            this._timer.Enabled = true;
            this._timer.SynchronizingObject = this;
            this._timer.Elapsed += new System.Timers.ElapsedEventHandler(this.OnTimerElapsed);
            ((System.ComponentModel.ISupportInitialize)(this._timer)).EndInit();
            this.ResumeLayout(false);
        }
    
        private void OnTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            // Console.WriteLine(".");
        }
    
    }

    然后将所有输入放在面板上。

      $p = New-Object TimerPanel
      $p.Size = $f.Size
      $end = (Get-Date -UFormat "%s")
      $end = ([int]$end + 60)
      $p.Timer.Stop()
      $p.Timer.Interval = 5000;
      $p.Timer.Start()
      $p.Timer.add_Elapsed({
          $start = (Get-Date -UFormat "%s")
    
          $elapsed = New-TimeSpan -Seconds ($start - $end)
          $l.Text = ('Remaining time {0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds,($end - $start))
    
          if ($end - $start -lt 0) {
            $caller.Data = $RESULT_TIMEOUT;
            $f.Close()
          }
    
        })

    Timer 的属性和方法是公共的,因此脚本提供了事件处理程序——在上面的示例中,一分钟的间隔(以秒为单位)被硬编码了。

    timing out prompt

    完整的示例显示在下面,并在源 zip 文件中提供。

    $RESULT_OK = 0
    $RESULT_CANCEL = 1
    $RESULT_TIMEOUT = 2
    
    $Readable = @{
      $RESULT_OK = 'OK';
      $RESULT_CANCEL = 'CANCEL';
      $RESULT_TIMEOUT = 'TIMEOUT';
    }
    
    function PromptTimedAutoClose  {
    
    param(
      [string]$title,
      [string]$message,
      [object]$caller
    )
    
      [void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
      [void][System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
    
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
    
      $f.Size = New-Object System.Drawing.Size (240,110)
    
      $f.StartPosition = 'CenterScreen'
    
      $f.KeyPreview = $True
      $f.Add_KeyDown({
    
          if ($_.KeyCode -eq 'O') { $caller.Data = $RESULT_OK }
          elseif ($_.KeyCode -eq 'Escape') { $caller.Data = $RESULT_CANCEL }
          else { return }
          $f.Close()
    
        })
    
      $b1 = New-Object System.Windows.Forms.Button
      $b1.Location = New-Object System.Drawing.Size (50,40)
      $b1.Size = New-Object System.Drawing.Size (75,23)
      $b1.Text = 'OK'
      $b1.add_click({ $caller.Data = $RESULT_OK; $f.Close(); })
      $p = New-Object TimerPanel
      $p.Size = $f.Size
    
      $p.Controls.Add($b1)
      $end = (Get-Date -UFormat "%s")
      $end = ([int]$end + 60)
      $b2 = New-Object System.Windows.Forms.Button
      $b2.Location = New-Object System.Drawing.Size (130,40)
      $b2.Size = New-Object System.Drawing.Size (75,23)
      $b2.Text = 'Cancel'
      $b2.add_click({
          $caller.Data = $RESULT_CANCEL;
          $f.Close();
        })
      $p.Controls.Add($b2)
    
      $l = New-Object System.Windows.Forms.Label
      $l.Location = New-Object System.Drawing.Size (10,20)
      $l.Size = New-Object System.Drawing.Size (280,20)
      $l.Text = $message
      $p.Controls.Add($l)
    
      $p.Timer.Stop()
      $p.Timer.Interval = 5000;
      $p.Timer.Start()
      $p.Timer.add_Elapsed({
          $start = (Get-Date -UFormat "%s")
    
          $elapsed = New-TimeSpan -Seconds ($start - $end)
          $l.Text = ('Remaining time {0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds,($end - $start))
    
          if ($end - $start -lt 0) {
            $caller.Data = $RESULT_TIMEOUT;
            $f.Close()
          }
    
        })
      $f.Controls.Add($p)
      $f.Topmost = $True
    
      $caller.Data = $RESULT_TIMEOUT;
      $f.Add_Shown({ $f.Activate() })
    
      [void]$f.ShowDialog([win32window ]($caller))
      $f.Dispose()
    }
    
    $DebugPreference = 'Continue'
    $title = 'Prompt w/timeout'
    $message = "Continue ?"
    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    
    PromptTimedAutoClose -Title $title -Message $message -caller $caller
    $result = $caller.Data
    Write-Debug ("Result is : {0} ({1})" -f $Readable.Item($result),$result)

    收集复选框和单选按钮组的选择

    button broups

    此示例代码更有趣,因为脚本将收集多个分组元素的的状态。管理单个 checkbox radiobutton 的行为保持不变,只实现按钮 Click 处理程序,其中窗体绘制选定元素的摘要并将其存储在 $caller 中——为简单起见,$shapes$color 都放入一个 $caller.Message 中。

    function PromptWithCheckboxesAndRadionbuttons(
        [String] $title,
        [String] $message,
        [Object] $caller = $null
        ){
    
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Data')
    
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
      $groupBox1 = New-Object System.Windows.Forms.GroupBox
      $checkBox1 = New-Object System.Windows.Forms.CheckBox
      $checkBox2 = New-Object System.Windows.Forms.CheckBox
      $checkBox3 = New-Object System.Windows.Forms.CheckBox
      $radioButton1 = New-Object System.Windows.Forms.RadioButton
      $radioButton2 = New-Object System.Windows.Forms.RadioButton
      $radioButton3 = New-Object System.Windows.Forms.RadioButton
      $button1  = New-Object System.Windows.Forms.Button
      $components =  New-Object System.ComponentModel.Container
    
      $groupBox1.SuspendLayout()
      $f.SuspendLayout()
      $color = ''
      $shapes = @()
    
      # groupBox1
      $groupBox1.Controls.AddRange(
         @(
           $radioButton1,
           $radioButton2,
           $radioButton3
          ))
      $groupBox1.Location = New-Object System.Drawing.Point(8, 120)
      $groupBox1.Name = 'groupBox1'
      $groupBox1.Size = New-Object System.Drawing.Size(120, 144)
      $groupBox1.TabIndex = 0
      $groupBox1.TabStop = $false
      $groupBox1.Text = 'Color'
    
      # checkBox1
      $checkBox1.Location = New-Object System.Drawing.Point(8, 8)
      $checkBox1.Name = 'checkBox1'
      $checkBox1.TabIndex = 1
      $checkBox1.Text = 'Circle'
    
      # checkBox2
    
      $checkBox2.Location = New-Object System.Drawing.Point(8, 40)
      $checkBox2.Name = 'checkBox2'
      $checkBox2.TabIndex = 2
      $checkBox2.Text = 'Rectangle'
    
      # checkBox3
    
      $checkBox3.Location = New-Object System.Drawing.Point(8, 72)
      $checkBox3.Name = 'checkBox3'
      $checkBox3.TabIndex = 3
      $checkBox3.Text = 'Triangle'
    
      # radioButton1
    
      $radioButton1.Location = New-Object System.Drawing.Point(8, 32)
      $radioButton1.Name = 'radioButton1'
      $radioButton1.TabIndex = 4
      $radioButton1.Text = 'Red'
      $radioButton1.Add_CheckedChanged({ })
    
      # radioButton2
    
      $radioButton2.Location = New-Object System.Drawing.Point(8, 64)
      $radioButton2.Name = 'radioButton2'
      $radioButton2.TabIndex = 5
      $radioButton2.Text = 'Green'
    
      # radioButton3
    
      $radioButton3.Location = New-Object System.Drawing.Point(8, 96)
      $radioButton3.Name = 'radioButton3'
      $radioButton3.TabIndex = 6
      $radioButton3.Text = 'Blue'
    
      # button1
    
      $button1.Location = New-Object System.Drawing.Point(8, 280)
      $button1.Name = 'button1'
      $button1.Size = New-Object System.Drawing.Size(112, 32)
      $button1.TabIndex = 4
      $button1.Text = 'Draw'
    
      $button1.Add_Click({
    
      $color = ''
      $shapes = @()
      foreach ($o in @($radioButton1, $radioButton2, $radioButton3)){
      if ($o.Checked){
          $color = $o.Text}
      }
      foreach ($o in @($checkBox1, $checkBox2, $checkBox3)){
      if ($o.Checked){
          $shapes += $o.Text}
    
      }
      $g = [System.Drawing.Graphics]::FromHwnd($f.Handle)
      $rc = New-Object System.Drawing.Rectangle(150, 50, 250, 250)
      $brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
      $g.FillRectangle($brush, $rc)
      $font = New-Object System.Drawing.Font('Verdana', 12)
      $col = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::Black)
      $str = [String]::Join(';', $shapes )
      $pos1 = New-Object System.Drawing.PointF(160, 60)
      $pos2 = New-Object System.Drawing.PointF(160, 80)
    
      $g.DrawString($color, $font, $col , $pos1)
      $g.DrawString($str, $font, $col , $pos2)
      start-sleep 1
    
      $caller.Message =  ('color:{0} shapes:{1}' -f $color , $str)
    
      $f.Close()
     })
    
      # Form1
    
      $f.AutoScaleBaseSize = New-Object System.Drawing.Size(5, 13)
      $f.ClientSize = New-Object System.Drawing.Size(408, 317)
      $f.Controls.AddRange( @(
         $button1,
         $checkBox3,
         $checkBox2,
         $checkBox1,
         $groupBox1))
    
      $f.Name = 'Form1'
      $f.Text = 'CheckBox and RadioButton Sample'
      $groupBox1.ResumeLayout($false)
      $f.ResumeLayout($false)
    
      $f.StartPosition = 'CenterScreen'
    
      $f.KeyPreview = $True
    
      $f.Add_KeyDown({
    
        if ($_.KeyCode -eq 'Escape')  { $caller.Data = $RESULT_CANCEL }
        else          {  }
        $f.Close()
      })
    
      $f.Topmost = $True
      if ($caller -eq $null ){
        $caller = New-Object Win32Window -ArgumentList
        ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
      }
    
      $f.Add_Shown( { $f.Activate() } )
    
      [Void] $f.ShowDialog([Win32Window ] ($caller) )
      $F.Dispose()
    
      return $caller.Data
    }

    列表框选择

    listboxes

    下一个迭代是让窗体从 PowerShell 接收文本字符串,并将单个单词显示为选中的 listbox 项,等待用户通过单击单词旁边的 checkbox 来选择各个单词。

    $DebugPreference = 'Continue'
    $result = PromptCheckedList ''  'Lorem ipsum dolor sit amet, consectetur adipisicing elit'
    
    write-debug ('Selection is : {0}' -f  , $result )

    右侧的 listbox 为用户提供视觉提示。按下“完成”按钮后,选择将保存在 $caller 对象中,窗体关闭并被处置。

    这次,我们显式返回 $caller.Message,尽管它不是真正必需的。请注意粗体显示的事件处理程序代码。

    function PromptCheckedList
    {
         Param(
        [String] $title,
        [String] $message)
    
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections.Generic')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Text')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Data')
    
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
    
      $i = new-object System.Windows.Forms.CheckedListBox
      $d = new-object System.Windows.Forms.ListBox
      $d.SuspendLayout()
      $i.SuspendLayout()
      $f.SuspendLayout()
      $i.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 11,
      [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);
      $i.FormattingEnabled = $true;
      $i.Items.AddRange(( $message -split '[ ,]+' ));
    
      $i.Location = New-Object System.Drawing.Point(17, 12)
      $i.Name = 'inputCheckedListBox'
      $i.Size = New-Object System.Drawing.Size(202, 188)
      $i.TabIndex = 0
      $i.TabStop = $false
    
      $event_handler = {
           param(
                [Object] $sender,
                [System.Windows.Forms.ItemCheckEventArgs ] $eventargs
             )
             $item = $i.SelectedItem
             if ( $eventargs.NewValue -eq  [System.Windows.Forms.CheckState]::Checked ) {
                $d.Items.Add( $item );
             } else {
                $d.Items.Remove( $item );
             }
      }
      $i.Add_ItemCheck($event_handler)
    
      $d.Font = New-Object System.Drawing.Font('Verdana', 11)
      $d.FormattingEnabled = $true
      $d.ItemHeight = 20;
      $d.Location =  New-Object System.Drawing.Point(236, 12);
      $d.Name = 'displayListBox';
      $d.Size = New-Object System.Drawing.Size(190, 184);
      $d.TabIndex = 1;
    
      $b  = New-Object System.Windows.Forms.Button
      $b.Location = New-Object System.Drawing.Point(8, 280)
      $b.Name = 'button1'
      $b.Size = New-Object System.Drawing.Size(112, 32)
      $b.TabIndex = 4
      $b.Text = 'Done'
    
      $b.Add_Click({
        $shapes = @()
        foreach ($o in $d.Items){
          $shapes += $o
        }
        $caller.Message =  [String]::Join(';', $shapes )
        $f.Close()
     })
    
      $f.AutoScaleBaseSize = New-Object System.Drawing.Size(5, 13)
      $f.ClientSize = New-Object System.Drawing.Size(408, 317)
      $components =  New-Object System.ComponentModel.Container
    
      $f.Controls.AddRange( @( $i, $d, $b))
    
      $f.Name = 'Form1'
      $f.Text = 'CheckListBox Sample'
      $i.ResumeLayout($false)
      $d.ResumeLayout($false)
    
      $f.ResumeLayout($false)
    
      $f.StartPosition = 'CenterScreen'
    
      $f.KeyPreview = $True
    
      $f.Topmost = $True
      $caller = New-Object Win32Window -ArgumentList
      ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    
      $f.Add_Shown( { $f.Activate() } )
    
      [Void] $f.ShowDialog([Win32Window ] ($caller) )
      $f.Dispose()
      $result = $caller.Message
      $caller = $null
      return $result
    }

    这里,事件处理程序是用 PowerShell 编写的,但它操作标准的事件参数,因此 PowerShell 函数是从 Form 元素调用的,基本上将它们相互连接起来。它几乎与已转换的类方法无法区分来自

    this.inputCheckedListBox.ItemCheck +=
    new System.Windows.Forms.ItemCheckEventHandler(this.inputCheckedListBox_ItemCheck);
    
    ...
    
    private void inputCheckedListBox_ItemCheck(object sender, ItemCheckEventArgs e )
          {
             string item = inputCheckedListBox.SelectedItem.ToString();
    
             if ( e.NewValue == CheckState.Checked )
                displayListBox.Items.Add( item );
             else
                displayListBox.Items.Remove( item );
          }

    手风琴菜单

    下一个示例来自将 手风琴可折叠面板 从 C# 转换为 PowerShell。当然,代码非常冗余。只显示一部分。完整脚本在源 zip 中。

    collapsed

    $caller = New-Object -TypeName 'Win32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    
    @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
    
    $f = New-Object -TypeName 'System.Windows.Forms.Form'
    $f.Text = $title
    $f.SuspendLayout()
    
    $p = New-Object System.Windows.Forms.Panel
    $m = New-Object System.Windows.Forms.Panel
    $p_3 = New-Object System.Windows.Forms.Panel
    $b_3_3 = New-Object System.Windows.Forms.Button
    $b_3_2 = New-Object System.Windows.Forms.Button
    $b_3_1 = New-Object System.Windows.Forms.Button
    $g_3 = New-Object System.Windows.Forms.Button
    $p_2 = New-Object System.Windows.Forms.Panel
    
    $b_2_4 = New-Object System.Windows.Forms.Button
    $b_2_3 = New-Object System.Windows.Forms.Button
    $b_2_2 = New-Object System.Windows.Forms.Button
    $b_2_1 = New-Object System.Windows.Forms.Button
    $g_2 = New-Object System.Windows.Forms.Button
    $p_1 = New-Object System.Windows.Forms.Panel
    $b_1_2 = New-Object System.Windows.Forms.Button
    $b_1_1 = New-Object System.Windows.Forms.Button
    $g_1 = New-Object System.Windows.Forms.Button
    $lblMenu = New-Object System.Windows.Forms.Label
    $m.SuspendLayout()
    $p_3.SuspendLayout()
    $p_2.SuspendLayout()
    $p_1.SuspendLayout()
    $p.SuspendLayout()
    
    ..
    #  Panel Menu 1
    $p_1.Controls.AddRange(@($b_1_2, $b_1_1,  $g_1) )
    $p_1.Dock = [System.Windows.Forms.DockStyle]::Top
    $p_1.Location = New-Object System.Drawing.Point (0,23)
    $p_1.Name = "p_1"
    # $p_1.Size = New-Object System.Drawing.Size ($global:button_panel_width,104)
    $p_1.TabIndex = 1
    
    #  Menu 1 button 1
    $b_1_1.BackColor = [System.Drawing.Color]::Silver
    $b_1_1.Dock = [System.Windows.Forms.DockStyle]::Top
    $b_1_1.FlatAppearance.BorderColor = [System.Drawing.Color]::DarkGray
    $b_1_1.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
    $b_1_1.Location = New-Object System.Drawing.Point (0,($global:button_panel_height * 2))
    
    $b_1_1.Name = "b_1_1"
    $b_1_1.Size = New-Object System.Drawing.Size ($global:button_panel_width,$global:button_panel_height)
    $b_1_1.TabIndex = 2
    $b_1_1.Text = "Group 1 Sub Menu 1"
    $b_1_1.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
    $b_1_1.UseVisualStyleBackColor = $false
    $b_1_1_click = $b_1_1.add_Click
    $b_1_1_click.Invoke({
    
        param([object]$sender,[string]$message)
        $caller.Data = $sender.Text
        [System.Windows.Forms.MessageBox]::Show(('{0} clicked!' -f $sender.Text) )
      })
    
    #  Menu 1 button 2
    $b_1_2.BackColor = [System.Drawing.Color]::Silver
    $b_1_2.Dock = [System.Windows.Forms.DockStyle]::Top
    $b_1_2.FlatAppearance.BorderColor = [System.Drawing.Color]::DarkGray
    $b_1_2.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
    $b_1_2.Location = New-Object System.Drawing.Point (0,($global:button_panel_height * 3))
    
    $b_1_2.Name = "$b_1_2"
    $b_1_2.Size = New-Object System.Drawing.Size ($global:button_panel_width,$global:button_panel_height)
    $b_1_2.TabIndex = 3
    $b_1_2.Text = "Group 1 Sub Menu 2"
    $b_1_2.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
    $b_1_2.UseVisualStyleBackColor = $false
    
    #  Menu 1 button group 
    $g_1.BackColor = [System.Drawing.Color]::Gray
    $g_1.Dock = [System.Windows.Forms.DockStyle]::Top
    $g_1.FlatAppearance.BorderColor = [System.Drawing.Color]::Gray
    $g_1.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
    $g_1.ImageAlign = [System.Drawing.ContentAlignment]::MiddleRight
    $g_1.Location = New-Object System.Drawing.Point (0,0)
    $g_1.Name = "g_1"
    $g_1.Size = New-Object System.Drawing.Size ($global:button_panel_width,$global:button_panel_height)
    $g_1.TabIndex = 0
    $g_1.Text = "Menu Group 1"
    $g_1.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
    $g_1.UseVisualStyleBackColor = $false
    $g_1_click = $g_1.add_click
    $g_1_click.Invoke({
        param(
          [object]$sender,
          [System.EventArgs]$eventargs
        )
    
        $ref_panel = ([ref]$p_1)
        $ref_button_menu_group = ([ref]$g_1)
        $num_buttons = 3
        # use the current height of the element as indicator of its state.
        if ($ref_panel.Value.Height -eq $global:button_panel_height)
        {
          $ref_panel.Value.Height = ($global:button_panel_height * $num_buttons) + 2
          $ref_button_menu_group.Value.Image = New-Object System.Drawing.Bitmap ("C:\developer\sergueik\powershell_ui_samples\unfinished\up.png")
        }
        else
        {
          $ref_panel.Value.Height = $global:button_panel_height
          $ref_button_menu_group.Value.Image = New-Object System.Drawing.Bitmap ("C:\developer\sergueik\powershell_ui_samples\unfinished\down.png")
        }
      })
    
    
    $m.ResumeLayout($false)
    $p_3.ResumeLayout($false)
    $p_2.ResumeLayout($false)
    $p_1.ResumeLayout($false)
    $p.ResumeLayout($false)
    
    
    $f.Controls.Add($p)
    #  Form1
    $f.AutoScaleDimensions = New-Object System.Drawing.SizeF (6.0,13.0)
    $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
    $f.ClientSize = New-Object System.Drawing.Size (210,280)
    $f.Controls.Add($c1)
    $f.Controls.Add($p)
    $f.Controls.Add($b1)
    $f.Name = "Form1"
    $f.Text = "ProgressCircle"
    $f.ResumeLayout($false)
    
    $f.Topmost = $True
    
    $f.Add_Shown({ $f.Activate() })
    
    [void]$f.ShowDialog([win32window]($caller))
    
    $f.Dispose()

    expanded

    为了对抗冗余,可以引入实用程序函数,例如

    function add_button {
      param(
        [System.Management.Automation.PSReference]$button_data_ref,
        [System.Management.Automation.PSReference]$button_ref
      )
    
      $button_data = $button_data_ref.Value
    
      #  TODO: assert ?
    
      $local:b = $button_ref.Value
      $local:b.BackColor = [System.Drawing.Color]::Silver
      $local:b.Dock = [System.Windows.Forms.DockStyle]::Top
      $local:b.FlatAppearance.BorderColor = [System.Drawing.Color]::DarkGray
      $local:b.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
      $local:b.Location = New-Object System.Drawing.Point (0,($global:button_panel_height * $button_data['cnt']))
      $local:b.Size = New-Object System.Drawing.Size ($global:button_panel_width,$global:button_panel_height)
      $local:b.TabIndex = 3
      $local:b.Name = $button_data['name']
      $local:b.Text = $button_data['text']
      $local:b.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
      $local:b.UseVisualStyleBackColor = $false
    
      $local:click_handler = $local:b.add_Click
      if ($button_data.ContainsKey('callback')) {
        $local:click_handler.Invoke($button_data['callback'])
      }
    
      else {
        # provide default click handler
    
        $local:click_handler.Invoke({
    
            param(
              [object]$sender,
              [System.EventArgs]$eventargs
            )
            $caller.Data = $sender.Text
            [System.Windows.Forms.MessageBox]::Show(('{0} default click handler!' -f $sender.Text))
          })
    
      }
      $button_ref.Value = $local:b
    }

    并重构代码以打包代码引用、菜单文本等。

    #  Menu 3 button 3
    # Provide a callback with  System.Windows.Forms.Button.OnClick Method argument signature
    [scriptblock]$b3_3_callback_ref = {
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
      $caller.Data = 'something'
      [System.Windows.Forms.MessageBox]::Show(('This is custom callback for {0} click!' -f $sender.Text))
    }
    
    add_button -button_ref ([ref]$b3_3) `
       -button_data_ref ([ref]@{
        'cnt' = 3;
        'text' = 'Menu 3 Sub Menu 3';
        'name' = 'b3_3';
        'callback' = $b3_3_callback_ref;
      })

    最终按钮数据对象和回调操作代码的布局当然高度依赖于领域。

    选中组合框

    下一个示例使用来自 带选中列表框作为下拉列表的组合框 的代码。与本文中的大多数示例不同,此脚本不使用 $caller 对象——CheckedComboBox 类本身有很多属性——而是通过引用将哈希对象传递给窗体来返回选定的文本数据。

    $albums = @{
    
      'Ring Ring (1973)' = $false;
      'Waterloo (1974)' = $false;
      'ABBA (1975)' = $true;
      'Arrival (1976)' = $false;
      'The Album (1977)' = $true;
      'Voulez-Vous (1979)' = $false;
      'Super Trouper (1980)' = $false;
      'The Visitors (1981)' = $false;
    }
    
    PromptCheckedCombo -Title 'Checked ComboBox  Sample Project' -data_ref ([ref]$albums)
    Write-Output ('Result is: {0}' -f $caller.Message)
    $albums

    这里函数的签名是

    function PromptCheckedCombo {
    
      param(
        [string]$title,
        [System.Management.Automation.PSReference]$data_ref  
    )
    ...
      $ccb = New-Object -TypeName 'CheckComboBoxTest.CheckedComboBox'
    
      $data = $data_ref.Value
      $cnt = 0
      $data.Keys | ForEach-Object { $display_item = $_;
    
        [CheckComboBoxTest.CCBoxItem]$item = New-Object CheckComboBoxTest.CCBoxItem ($display_item,$cnt)
        $ccb.Items.Add($item) | Out-Null
        if ($data[$display_item]) {
          $ccb.SetItemChecked($cnt,$true)
        }
        $cnt++
      }

    result

    在窗体委托中,迭代引用数据的键,并清除/设置哈希值。

      $eventMethod_ccb = $ccb.add_DropDownClosed
      $eventMethod_ccb.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
    
          $data = $data_ref.Value
          $data.Keys | ForEach-Object {
            $display_item = $_;
            $data_ref.Value[$display_item] = $false
          }
          foreach ($item in $ccb.CheckedItems) {
            $data_ref.Value[$item.Name] = $true
          }
    
          $data_ref.Value = $data
    
        })

    result

    条形图

    下一个示例显示了自定义绘制的条形图,它没有第三方图表库依赖。使用了来自 Drawing a Bar Chart 文章的 VB.NET 示例代码,并进行了一些小的重构和修改。

    Add-Type -Language 'VisualBasic' -TypeDefinition  @"
    Imports Microsoft.VisualBasic
    Imports System
    Imports System.Drawing
    Imports System.Drawing.Drawing2D
    Imports System.Collections
    Imports System.Windows.Forms
    
    Public Class BarChart
    
        Inherits System.Windows.Forms.Form
    
        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub
    
        Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
            If disposing Then
                If Not (components Is Nothing) Then
                    components.Dispose()
                End If
            End If
            MyBase.Dispose(disposing)
        End Sub
    
        Private components As System.ComponentModel.IContainer
    
        <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
            Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
            Me.ClientSize = New System.Drawing.Size(344, 302)
            Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable
            Me.Name = "BarChart"
            Me.Text = "BarChart"
            Me.components = New System.ComponentModel.Container
            Me.ttHint = New System.Windows.Forms.ToolTip(Me.components)
        End Sub
    
        Dim blnFormLoaded As Boolean = False
        Dim objHashTableG As New Hashtable(100)
    
        Dim objColorArray(150) As Brush
        Private Sub BarChart_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    
        End Sub
    
        Public Sub LoadData(ByVal objCallerHashTable As Hashtable )
           objHashTableG =  objCallerHashTable.Clone()
        End Sub
    
    
        Public Sub RenderData 
            Me.BarChart_Paint(Nothing, New System.Windows.Forms.PaintEventArgs( _
            CreateGraphics(), _
    	New System.Drawing.Rectangle(0, 0, Me.Width, Me.Height) _
            )) 
        End Sub
    
        Private Sub BarChart_Paint(ByVal sender As Object, _
                                   ByVal e As System.Windows.Forms.PaintEventArgs _
                                   ) Handles MyBase.Paint
            Try
                Dim intMaxWidth As Integer
                Dim intMaxHeight As Integer
                Dim intXaxis As Integer
                Dim intYaxis As Integer
                Me.SuspendLayout()
                Me.LoadColorArray()
                intMaxHeight = CType((Me.Height / 2) - (Me.Height / 12), Integer)
                intMaxWidth = CType(Me.Width - (Me.Width / 4), Integer)
                intXaxis = CType(Me.Width / 12, Integer)
                intYaxis = CType(Me.Height / 2, Integer)
                drawBarChart(objHashTableG.GetEnumerator , _
                             objHashTableG.Count, _
                             "Graph 1", _
                             intXaxis, _
                             intYaxis, _
                             intMaxWidth, _
                             intMaxHeight, _
                             True, _
                             False)
                blnFormLoaded = True
                Me.ResumeLayout(False)
            Catch ex As Exception
                Throw ex
            End Try
            
        End Sub
    
        Public Sub drawBarChart(ByVal objEnum As IDictionaryEnumerator, _
                                ByVal intItemCount As Integer, _
                                ByVal strGraphTitle As String, _
                                ByVal Xaxis As Integer, _
                                ByVal Yaxis As Integer, _
                                ByVal MaxWidth As Int16, _
                                ByVal MaxHt As Int16, _
                                ByVal clearForm As Boolean, _
                                Optional ByVal SpaceRequired As Boolean = False)
    
            Dim intGraphXaxis As Integer = Xaxis
            Dim intGraphYaxis As Integer = Yaxis
            Dim intWidthMax As Integer = MaxWidth
            Dim intHeightMax As Integer = MaxHt
            Dim intSpaceHeight As Integer
            Dim intMaxValue As Integer = 0
            Dim intCounter As Integer
            Dim intBarWidthMax
            Dim intBarHeight
            Dim strText As String
            Try
                Dim grfx As Graphics = CreateGraphics()
                If clearForm = True Then
                    grfx.Clear(BackColor)
                End If
    
                grfx.DrawString(strGraphTitle, New Font("Verdana", 12.0, FontStyle.Bold, GraphicsUnit.Point), Brushes.DeepPink, intGraphXaxis + (intWidthMax / 4), (intGraphYaxis - intHeightMax) - 40)
    
                'Get the Height of the Bar        
                intBarHeight = CInt(intHeightMax / intItemCount)
    
                'Get the space Height of the Bar 
                intSpaceHeight = CInt((intHeightMax / (intItemCount - 1)) - intBarHeight)
    
                'Find Maximum of the input value
                If Not objEnum Is Nothing Then
                    While objEnum.MoveNext = True
                        If objEnum.Value > intMaxValue Then
                            intMaxValue = objEnum.Value
                        End If
                    End While
                End If
    
                'Get the Maximum Width of the Bar
                intBarWidthMax = CInt(intWidthMax / intMaxValue)
    
                ' Obtain the Graphics object exposed by the Form.
                If Not objEnum Is Nothing Then
                    intCounter = 1
                    objEnum.Reset()
                    'Draw X axis and Y axis lines
                    'grfx.DrawLine(Pens.Black, intGraphXaxis, intGraphYaxis, intGraphXaxis + intWidthMax, intGraphYaxis)
                    'grfx.DrawLine(Pens.Black, intGraphXaxis, intGraphYaxis, intGraphXaxis, (intGraphYaxis - intHeightMax) - 25)
    
                    While objEnum.MoveNext = True
                        'Get new Y axis
                        intGraphYaxis = intGraphYaxis - intBarHeight
    
                        Dim objRec as Rectangle
                        objRec = New System.Drawing.Rectangle(intGraphXaxis, intGraphYaxis, intBarWidthMax * objEnum.Value, intBarHeight)
                        'Draw Rectangle
                        grfx.DrawRectangle(Pens.Black, objRec)
                        'Fill Rectangle
                        grfx.FillRectangle(objColorArray(intCounter), objRec )
                        'Display Text and value
                        ' http://www.java2s.com/Tutorial/VB/0300__2D-Graphics/Measurestringanddrawstring.htm
                        strText =  objEnum.Key & "=" & objEnum.Value 
                        Dim objLabelFont as Font
                        objLabelFont = New Font("Verdana", 7.2, FontStyle.Regular, GraphicsUnit.Point) 
                        Dim textLabelArea As SizeF : textLabelArea = grfx.MeasureString(strText, objLabelFont)
    
                        Dim linePen As Pen: linePen = New Pen(Color.Gray, 1)
                        linePen.DashStyle = Drawing2D.DashStyle.Dash
    
                        Dim fontRatio As Single 
                        fontRatio = objLabelFont.Height / objLabelFont.FontFamily.GetLineSpacing(FontStyle.Regular)
     
                        Dim ascentSize As Single
                        ascentSize = objLabelFont.FontFamily.GetCellAscent(FontStyle.Regular) * fontRatio
                        Dim descentSize As Single
                        descentSize = objLabelFont.FontFamily.GetCellDescent(FontStyle.Regular) * fontRatio
                        Dim emSize As Single
                        emSize = objLabelFont.FontFamily.GetEmHeight(FontStyle.Regular) * fontRatio
                        Dim cellHeight As Single
                        cellHeight = ascentSize + descentSize
                        Dim internalLeading As Single
                        internalLeading = cellHeight - emSize
                        Dim externalLeading As Single
                        externalLeading = (objLabelFont.FontFamily.GetLineSpacing(FontStyle.Regular) * fontRatio) - cellHeight
    
    
                        Dim labelLeft As Single : labelLeft = intGraphXaxis + (intBarWidthMax * objEnum.Value)
                        labelLeft = intGraphXaxis
                        Dim labelBottom As Single:  labelBottom =  intGraphYaxis
                        Dim labelRight As Single : labelRight = labelLeft + textLabelArea.Width
                        Dim labelTop As Single : labelTop = textLabelArea.Height + labelBottom
    
                        Dim objLabelRec as Rectangle
                        objLabelRec = New System.Drawing.Rectangle(labelLeft, labelBottom, textLabelArea.Width , textLabelArea.Height )
                     
                        grfx.DrawRectangle(Pens.Black, objLabelRec)
                        'Fill Rectangle
                        grfx.FillRectangle(Brushes.White, objLabelRec )
    
                        grfx.DrawLine(linePen, labelLeft, labelTop, labelLeft , labelBottom)
                        grfx.DrawLine(linePen, labelRight, labelTop, labelRight , labelBottom)
                        grfx.DrawLine(linePen, labelLeft, labelTop, labelRight , labelTop)
                        grfx.DrawLine(linePen, labelLeft, labelBottom, labelRight , labelBottom)
                        grfx.DrawString(strText, objLabelFont, Brushes.Black, labelLeft, labelBottom)
    
                        intCounter += 1
                        If SpaceRequired = True Then
                            intGraphYaxis = intGraphYaxis - intSpaceHeight
                        End If
                        If intCounter > objColorArray.GetUpperBound(0) Then
                            intCounter = 1
                        End If
                    End While
                    If clearForm = True Then
                        grfx.Dispose()
                    End If
                End If
            Catch ex As Exception
                Throw ex
            End Try
        End Sub
    
        Public Sub LoadColorArray()
            objColorArray(1) = Brushes.Blue
            objColorArray(2) = Brushes.Pink
            objColorArray(3) = Brushes.Brown
            objColorArray(4) = Brushes.BurlyWood
            objColorArray(5) = Brushes.CadetBlue
            objColorArray(6) = Brushes.Chartreuse
            objColorArray(7) = Brushes.Chocolate
            objColorArray(8) = Brushes.Coral
            objColorArray(9) = Brushes.CornflowerBlue
            objColorArray(10) = Brushes.Cornsilk
            objColorArray(11) = Brushes.Crimson
            objColorArray(12) = Brushes.Cyan
            objColorArray(13) = Brushes.DarkBlue
            objColorArray(14) = Brushes.DarkCyan
            objColorArray(15) = Brushes.DarkGoldenrod
            objColorArray(16) = Brushes.DarkGray
            objColorArray(17) = Brushes.DarkGreen
            objColorArray(18) = Brushes.DarkKhaki
            objColorArray(19) = Brushes.DarkMagenta
            objColorArray(20) = Brushes.DarkOliveGreen
            objColorArray(21) = Brushes.DarkOrange
            objColorArray(22) = Brushes.DarkOrchid
            objColorArray(23) = Brushes.DarkRed
            objColorArray(24) = Brushes.DarkSalmon
            objColorArray(25) = Brushes.DarkSeaGreen
            objColorArray(26) = Brushes.DarkSlateBlue
            objColorArray(27) = Brushes.DarkSlateGray
            objColorArray(28) = Brushes.DarkTurquoise
            objColorArray(29) = Brushes.DarkViolet
            objColorArray(30) = Brushes.DeepPink
            objColorArray(31) = Brushes.DeepSkyBlue
            objColorArray(32) = Brushes.DimGray
            objColorArray(33) = Brushes.DodgerBlue
            objColorArray(34) = Brushes.Firebrick
            objColorArray(35) = Brushes.FloralWhite
            objColorArray(36) = Brushes.ForestGreen
            objColorArray(37) = Brushes.Fuchsia
            objColorArray(38) = Brushes.Gainsboro
            objColorArray(39) = Brushes.GhostWhite
            objColorArray(40) = Brushes.Gold
            objColorArray(41) = Brushes.Goldenrod
            objColorArray(42) = Brushes.Gray
            objColorArray(43) = Brushes.Green
            objColorArray(44) = Brushes.GreenYellow
            objColorArray(45) = Brushes.Honeydew
            objColorArray(46) = Brushes.HotPink
            objColorArray(47) = Brushes.IndianRed
            objColorArray(48) = Brushes.Indigo
            objColorArray(49) = Brushes.Ivory
            objColorArray(50) = Brushes.Khaki
            objColorArray(51) = Brushes.Lavender
            objColorArray(52) = Brushes.LavenderBlush
            objColorArray(53) = Brushes.LawnGreen
            objColorArray(54) = Brushes.LemonChiffon
            objColorArray(55) = Brushes.LightBlue
            objColorArray(56) = Brushes.LightCoral
            objColorArray(57) = Brushes.LightCyan
            objColorArray(58) = Brushes.LightGoldenrodYellow
            objColorArray(59) = Brushes.LightGray
            objColorArray(60) = Brushes.LightGreen
            objColorArray(61) = Brushes.LightPink
            objColorArray(62) = Brushes.LightSalmon
            objColorArray(63) = Brushes.LightSeaGreen
            objColorArray(64) = Brushes.LightSkyBlue
            objColorArray(65) = Brushes.LightSlateGray
            objColorArray(66) = Brushes.LightSteelBlue
            objColorArray(67) = Brushes.LightYellow
            objColorArray(68) = Brushes.Lime
            objColorArray(69) = Brushes.LimeGreen
            objColorArray(70) = Brushes.Linen
            objColorArray(71) = Brushes.Magenta
            objColorArray(72) = Brushes.Maroon
            objColorArray(73) = Brushes.MediumAquamarine
            objColorArray(74) = Brushes.MediumBlue
            objColorArray(75) = Brushes.MediumOrchid
            objColorArray(76) = Brushes.MediumPurple
            objColorArray(77) = Brushes.MediumSeaGreen
            objColorArray(78) = Brushes.MediumSlateBlue
            objColorArray(79) = Brushes.MediumSpringGreen
            objColorArray(80) = Brushes.MediumTurquoise
            objColorArray(81) = Brushes.MediumVioletRed
            objColorArray(82) = Brushes.MidnightBlue
            objColorArray(83) = Brushes.MintCream
            objColorArray(84) = Brushes.MistyRose
            objColorArray(85) = Brushes.Moccasin
            objColorArray(86) = Brushes.NavajoWhite
            objColorArray(87) = Brushes.Navy
            objColorArray(88) = Brushes.OldLace
            objColorArray(89) = Brushes.Olive
            objColorArray(90) = Brushes.OliveDrab
            objColorArray(91) = Brushes.Orange
            objColorArray(92) = Brushes.OrangeRed
            objColorArray(93) = Brushes.Orchid
            objColorArray(94) = Brushes.PaleGoldenrod
            objColorArray(95) = Brushes.PaleGreen
            objColorArray(96) = Brushes.PaleTurquoise
            objColorArray(97) = Brushes.PaleVioletRed
            objColorArray(98) = Brushes.PapayaWhip
            objColorArray(99) = Brushes.PeachPuff
            objColorArray(100) = Brushes.Peru
            objColorArray(101) = Brushes.Pink
            objColorArray(102) = Brushes.Plum
            objColorArray(103) = Brushes.PowderBlue
            objColorArray(104) = Brushes.Purple
            objColorArray(105) = Brushes.Red
            objColorArray(106) = Brushes.RosyBrown
            objColorArray(107) = Brushes.RoyalBlue
            objColorArray(108) = Brushes.SaddleBrown
            objColorArray(109) = Brushes.Salmon
            objColorArray(110) = Brushes.SandyBrown
            objColorArray(111) = Brushes.SeaGreen
            objColorArray(112) = Brushes.SeaShell
            objColorArray(113) = Brushes.Sienna
            objColorArray(114) = Brushes.Silver
            objColorArray(115) = Brushes.SkyBlue
            objColorArray(116) = Brushes.SlateBlue
            objColorArray(117) = Brushes.SlateGray
            objColorArray(118) = Brushes.Snow
            objColorArray(119) = Brushes.SpringGreen
            objColorArray(120) = Brushes.SteelBlue
            objColorArray(121) = Brushes.Tan
            objColorArray(122) = Brushes.Teal
            objColorArray(123) = Brushes.Thistle
            objColorArray(124) = Brushes.Tomato
            objColorArray(125) = Brushes.Transparent
            objColorArray(126) = Brushes.Turquoise
            objColorArray(127) = Brushes.Violet
            objColorArray(128) = Brushes.Wheat
            objColorArray(129) = Brushes.White
            objColorArray(130) = Brushes.WhiteSmoke
            objColorArray(131) = Brushes.Yellow
            objColorArray(132) = Brushes.YellowGreen
        End Sub
        Private Sub BarChart_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Resize
            If blnFormLoaded = True Then
                BarChart_Paint(Me, New System.Windows.Forms.PaintEventArgs(CreateGraphics(), New System.Drawing.Rectangle(0, 0, Me.Width, Me.Height)))
            End If
        End Sub
        Friend WithEvents ttHint As System.Windows.Forms.ToolTip
        ' Friend WithEvents RecLabel As System.Windows.Forms.Label
        '' need to draw System.Windows.Forms.Control
    End Class
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll', 'System.Drawing.dll', 'System.Drawing.dll'

    在此演示中,PowerShell 打开窗体并将两个数据样本发送给它,在每个样本渲染后等待几秒钟,然后关闭窗体。

    grid

    $object = New-Object -TypeName 'BarChart'
    
    $data1 = New-Object System.Collections.Hashtable(10)
    $data1.Add("Product1", 25)
    $data1.Add("Product2", 15)
    $data1.Add("Product3", 35)
    $object.LoadData([System.Collections.Hashtable] $data1)
    
    [void]$object.Show()
    start-sleep -seconds 5
    
    $data2 =  New-Object System.Collections.Hashtable(100)
    $data2.Add("Item1", 50)
    $data2.Add("Item2", 150)
    $data2.Add("Item3", 250)
    $data2.Add("Item4", 20)
    $data2.Add("Item5", 100)
    $data2.Add("Item6", 125)
    $data2.Add("Item7", 148)
    $data2.Add("Item8", 199)
    $data2.Add("Item9", 267)
    
    $object.LoadData([System.Collections.Hashtable] $data2)
    
    $object.RenderData()
    start-sleep -seconds 5
    
    $object.Close()
    $object.Dispose()

    已向该窗体添加了两个公共方法 LoadDataRenderData 以允许从脚本控制窗体。为了防止修改原始示例,第一个方法克隆了来自调用者的数据,而后者创建了一个虚拟的事件参数并调用了处理程序。

        Public Sub LoadData(ByVal objCallerHashTable As Hashtable )
           objHashTableG =  objCallerHashTable.Clone()
        End Sub
    
    
        Public Sub RenderData 
            Me.BarChart_Paint(Nothing, New System.Windows.Forms.PaintEventArgs( _
            CreateGraphics(), _
    	New System.Drawing.Rectangle(0, 0, Me.Width, Me.Height) _
            )) 
        End Sub

    没有从窗体到脚本的通信,因此不需要实现 IWin32Window 的单独对象。为了举例,下面仍然提供了 VB.Net 版本。

    Add-Type -Language 'VisualBasic' -TypeDefinition @"
    Public Class MyWin32Window 
    Implements System.Windows.Forms.IWin32Window
    
        Dim _hWnd As System.IntPtr
    
        Public Sub New(ByVal handle As System.IntPtr)
    
           _hWnd = handle
    
        End Sub
    
        Public ReadOnly Property Handle() As System.IntPtr Implements System.Windows.Forms.IWin32Window.Handle
            Get
                Handle = _hWnd
            End Get
        End Property
     
    End Class
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll'
    $caller = New-Object -TypeName 'MyWin32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

    图表的真实世界数据

    为了为主图表(也可以通过甘特图渲染数据)提供真实世界的数据样本,您需要捕获网站页面元素的加载时间以进行性能测量。这可以通过 FiddlerCore 程序集轻松实现。脚本的 c# 部分包含一个修改过的 fiddlercore-demo 示例,重点关注 Fiddler 返回的部分指标。

    Add-Type @"
    
    using System;
    using Fiddler;
    
    namespace WebTester
    {
        public class Monitor
        {
            public Monitor()
            {
                #region AttachEventListeners
    
                // Simply echo notifications to the console.  Because CONFIG.QuietMode=true 
                // by default, we must handle notifying the user ourselves.
                FiddlerApplication.OnNotification += delegate(object sender, NotificationEventArgs oNEA) { Console.WriteLine("** NotifyUser: " + oNEA.NotifyString); };
                FiddlerApplication.Log.OnLogString += delegate(object sender, LogEventArgs oLEA) { Console.WriteLine("** LogString: " + oLEA.LogString); };
    
                FiddlerApplication.BeforeRequest += (s) =>
                {
                    // In order to enable response tampering, buffering mode must
                    // be enabled; this allows FiddlerCore to permit modification of
                    // the response in the BeforeResponse handler rather than streaming
                    // the response to the client as the response comes in.
                    s.bBufferResponse = true;
                };
    
                FiddlerApplication.BeforeResponse += (s) =>
                {
                    // Uncomment the following to decompress/unchunk the HTTP response 
                    // s.utilDecodeResponse(); 
                };
    
    
                FiddlerApplication.AfterSessionComplete += (fiddler_session) =>
                {
                    // Ignore HTTPS connect requests
                    if (fiddler_session.RequestMethod == "CONNECT")
                        return;
    
                    if (fiddler_session == null || fiddler_session.oRequest == null || fiddler_session.oRequest.headers == null)
                        return;
    
                    var full_url = fiddler_session.fullUrl;
                    Console.WriteLine("URL: " + full_url);
    
                    HTTPResponseHeaders response_headers = fiddler_session.ResponseHeaders;
                    Console.WriteLine("HTTP Response: " + response_headers.HTTPResponseCode.ToString());
                    /*
                    foreach (HTTPHeaderItem header_item in response_headers){
                       Console.WriteLine(header_item.Name + " " + header_item.Value);
                    }
                        */
                    // http://fiddler.wikidot.com/timers
                    var timers = fiddler_session.Timers;
                    var duration = timers.ClientDoneResponse - timers.ClientBeginRequest;
                    Console.WriteLine(String.Format("Duration: {0:F10}", duration.Milliseconds));
    
                };
                #endregion AttachEventListeners
            }
    
    
            public void Start()
            {
                Console.WriteLine("Starting FiddlerCore...");
                // For the purposes of this demo, we'll forbid connections to HTTPS 
                // sites that use invalid certificates
                CONFIG.IgnoreServerCertErrors = false;
                // Because we've chosen to decrypt HTTPS traffic, makecert.exe must
                // be present in the Application folder.
                FiddlerApplication.Startup(8877, true, true);
                Console.WriteLine("Hit CTRL+C to end session.");
                // Wait Forever for the user to hit CTRL+C.  
                // BUG BUG: Doesn't properly handle shutdown of Windows, etc.
            }
    
            public void Stop()
            {
                Console.WriteLine("Shutdown.");
                FiddlerApplication.Shutdown();
                System.Threading.Thread.Sleep(1);
            }
            public static Monitor m;
    
            static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
            {
                Console.WriteLine("Stop.");
                m.Stop();
                System.Threading.Thread.Sleep(1);
            }
        }
    }
    
    "@ -ReferencedAssemblies 'System.dll','System.Data.dll',"${shared_assemblies_path}\FiddlerCore4.dll"
    

    修改主要是在 AfterSessionComplete 委托中进行的。这个类嵌入在 PowerShell 中,并设置为监听大约 $selenium.Navigate().GoToUrl($base_url) 调用期间的流量。

    $o = New-Object -TypeName 'WebTester.Monitor'
    $o.Start()
    # ...  initialize $selenium ...
    $selenium.Navigate().GoToUrl($base_url)
    $o.Stop()
    [bool]$fullstop = [bool]$PSBoundParameters['pause'].IsPresent
    
    

    收集持续时间的另一种方法是通过 Selenium 在 Chrome 浏览器中调用。

    using System;
    using System.Text.RegularExpressions;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.IO;
    using OpenQA.Selenium;
    using OpenQA.Selenium.Chrome;
    using OpenQA.Selenium.Remote;
    
    namespace WebTester
    {
    
            // http://stackoverflow.com/questions/6229769/execute-javascript-using-selenium-webdriver-in-c-sharp
            // http://stackoverflow.com/questions/14146513/selenium-web-driver-c-sharp-invalidcastexception-for-list-of-webelements-after-j
            // http://stackoverflow.com/questions/8133661/checking-page-load-time-of-several-url-simultaneously
            // http://blogs.msdn.com/b/fiddler/archive/2011/02/10/fiddler-is-better-with-internet-explorer-9.aspx
    
        public static class Extensions
        {
            static int cnt = 0;
    
            public static T Execute(this IWebDriver driver, string script)
            {
                return (T)((IJavaScriptExecutor)driver).ExecuteScript(script);
            }
    
    		        // http://stackoverflow.com/questions/6229769/execute-javascript-using-selenium-webdriver-in-c-sharp
            // http://stackoverflow.com/questions/14146513/selenium-web-driver-c-sharp-invalidcastexception-for-list-of-webelements-after-j
            // http://stackoverflow.com/questions/8133661/checking-page-load-time-of-several-url-simultaneously
            // http://blogs.msdn.com/b/fiddler/archive/2011/02/10/fiddler-is-better-with-internet-explorer-9.aspx
    
            public static List> Performance(this IWebDriver driver)
            {
                // NOTE: performance.getEntries is only with Chrome
                // performance.timing is available for FF and PhantomJS
    
                string performance_script = @"
    var ua = window.navigator.userAgent;
    
    if (ua.match(/PhantomJS/)) {
        return 'Cannot measure on ' + ua;
    } else {
        var performance =
            window.performance ||
            window.mozPerformance ||
            window.msPerformance ||
            window.webkitPerformance || {};
    
        // var timings = performance.timing || {};
        // return timings;
        var network = performance.getEntries() || {};
        return network;
    }
    ";
                List> result = new List>();
                IEnumerable<Object> raw_data = driver.Execute>(performance_script);
    
                foreach (var element in (IEnumerable<Object>)raw_data)
                {
                    Dictionary row = new Dictionary();
                    Dictionary dic = (Dictionary)element;
                    foreach (object key in dic.Keys)
                    {
                        Object val = null;
                        if (!dic.TryGetValue(key.ToString(), out val)) { val = ""; }
                        row.Add(key.ToString(), val.ToString());
                    }
                    result.Add(row);
                }
                return result;
            }
    
    public static void WaitDocumentReadyState(
    /* this // no longer is an extension method  */ 
    IWebDriver driver, string expected_state, int max_cnt = 10)
            {
                cnt = 0;
                var wait = new OpenQA.Selenium.Support.UI.WebDriverWait(driver, TimeSpan.FromSeconds(30.00));
                wait.PollingInterval = TimeSpan.FromSeconds(0.50);
                wait.Until(dummy =>
                {
                    string result = driver.Execute("return document.readyState").ToString();
                    Console.Error.WriteLine(String.Format("result = {0}", result));
                    Console.WriteLine(String.Format("cnt = {0}", cnt));
                    cnt++;
                    // TODO: match
                    return ((result.Equals(expected_state) || cnt > max_cnt));
                });
            }
        }
    
    }
    
    
    $selenium.Navigate().GoToUrl($base_url)
    $expected_states = @( "interactive", "complete" );
    [WebTester.Extensions]::WaitDocumentReadyState($selenium, $expected_states[1])
    $script = @"
    var ua = window.navigator.userAgent;
    
    if (ua.match(/PhantomJS/)) { 
    return 'Cannot measure on '+ ua;
    }
    else{
    var performance = 
          window.performance || 
          window.mozPerformance || 
          window.msPerformance || 
          window.webkitPerformance || {}; 
    // var timings = performance.timing || {};
    // return timings;
    // NOTE:  performance.timing will not return anything with Chrome
    // timing is returned by FF
    // timing is returned by Phantom
    var network = performance.getEntries() || {}; 
     return network;
    }
    
    
    "@
    
    # executeScript works fine with Chrome or Firefox 31, ie 10, but not IE 11.
    # Exception calling "ExecuteScript" with "1" argument(s): "Unable to get browser
    # https://code.google.com/p/selenium/issues/detail?id=6511  
    # 
    # https://code.google.com/p/selenium/source/browse/java/client/src/org/openqa/selenium/remote/HttpCommandExecutor.java?r=3f4622ced689d2670851b74dac0c556bcae2d0fe
    
    $savedata = $true
    
    if ($headless) {
      # for PhantomJS more work is needed
      # https://github.com/detro/ghostdriver/blob/master/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSDriver.java
      $result = ([OpenQA.Selenium.PhantomJS.PhantomJSDriver]$selenium).ExecutePhantomJS($script,[System.Object[]]@())
      $result | Format-List
      return
    } else {
    
      $result = ([OpenQA.Selenium.IJavaScriptExecutor]$selenium).executeScript($script)
      # $result | get-member 
    
      $result | ForEach-Object {
        $element_result = $_
        # $element_result | format-list
        Write-Output $element_result.Name
        Write-Output $element_result.duration
        $o = New-Object PSObject
        $caption = 'test'
        $o | Add-Member Noteproperty 'url' $element_result.Name
        $o | Add-Member Noteproperty 'caption' $caption
        $o | Add-Member Noteproperty 'load_time' $element_result.duration
        $o | Format-List
    
        if ($savedata) {
          insert_database3 -data $o -database "$script_directory\timings.db"
        }
        $o = $null
    

    完整的脚本可在附加的 zip 文件中找到。

    折线图、条形图和饼图

    下一个示例展示了另一个自定义绘制的 折线图、条形图和饼图库,它也实现在一个 C# 类中。

    result

    Add-Type @"
    
    // "
     #region PROGRAM HEADER
    /*
    *********************************************************************************************
    * FILE NAME      : DrawGraph.cs                                                             *
    * DESCRIPTION    : Generates Bar, Line & Pie graph for a set of values [maximum limit= 10]  *
    * AUTHOR         : Anoop Unnikrishnan   (AUK) 
    // ... currently we use unmodified code ...
    
    
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll','System.Drawing.dll','System.Data.dll','System.Xml.dll'

    窗体仅限于选择图表形状。请注意,中还有更多形状(此处未显示)。

    function DrawGraph {
    
      param(
        [string]$title,
        [System.Management.Automation.PSReference]$data_ref,
        [object]$caller
      )
    
      @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
    
    
      $f.Size = New-Object System.Drawing.Size (470,385)
      $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
      $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
      $f.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
      $f.SuspendLayout()
    
      $o = New-Object -TypeName 'System.Anoop.Graph.DrawGraph' -ArgumentList @( [string[]]$data_ref.Value.Keys,
        [float[]]$data_ref.Value.Values,
        $null,
        $null,
        'Arial',
        200
      )
      [System.Windows.Forms.PictureBox]$b = New-Object -TypeName 'System.Windows.Forms.PictureBox'
      $b.Location = New-Object System.Drawing.Point (40,20)
      $b.Name = 'p5'
      $b.Size = New-Object System.Drawing.Size (($f.Size.Width - 20),($f.Size.Height - 100))
      $b.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::AutoSize
      $b.TabIndex = 1
      $b.TabStop = $false
    
      $m = New-Object -TypeName 'System.Windows.Forms.MenuStrip'
      $file_m1 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
      $shape_m1 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
      $shape_m2 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
      $shape_m3 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
      $exit_m1 = New-Object -TypeName 'System.Windows.Forms.ToolStripMenuItem'
      $m.SuspendLayout()
    
      #  m0
      $m.Items.AddRange(@( $file_m1,$exit_m1))
      $m.Location = New-Object System.Drawing.Point (0,0)
      $m.Name = "m0"
      $m.Size = New-Object System.Drawing.Size (($f.Size.Width),24)
      $m.TabIndex = 0
      $m.Text = "m0"
    
    
      #  ShapeToolStripMenuItem
      $shape_m1.Name = "LineGraphToolStripMenuItem"
      $shape_m1.Text = "Line Graph"
    
      $eventMethod_shape_m1 = $shape_m1.add_click
      $eventMethod_shape_m1.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          $who = $sender.Text
          # [System.Windows.Forms.MessageBox]::Show(("We are processing {0}." -f $who))
          $b.Image = $o.DrawLineGraph()
          $caller.Data = $sender.Text
        })
    
      $shape_m2.Name = "BarGraphToolStripMenuItem"
      $shape_m2.Text = "Bar Graph"
    
      $eventMethod_shape_m2 = $shape_m2.add_click
      $eventMethod_shape_m2.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          $who = $sender.Text
          # [System.Windows.Forms.MessageBox]::Show(("We are processing {0}." -f $who))
          $b.Image = $o.DrawBarGraph()
          $caller.Data = $sender.Text
        })
    
      $shape_m3.Name = "3dPieChartToolStripMenuItem"
      $shape_m3.Text = "3d Pie Chart"
    
      $eventMethod_shape_m3 = $shape_m3.add_click
      $eventMethod_shape_m3.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          $who = $sender.Text
          # [System.Windows.Forms.MessageBox]::Show(("We are processing {0}." -f $who))
          $b.Image = $o.Draw3DPieGraph()
          $caller.Data = $sender.Text
        })
    
      # Separator 
      $dash = New-Object -TypeName System.Windows.Forms.ToolStripSeparator
    
      #  exitToolStripMenuItem
      $exit_m1.Name = "exitToolStripMenuItem"
      $exit_m1.Text = "Exit"
    
      $eventMethod_exit_m1 = $exit_m1.add_click
      $eventMethod_exit_m1.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          $who = $sender.Text
          # [System.Windows.Forms.MessageBox]::Show(("We are processing {0}." -f $who))
          $caller.Data = $sender.Text
          $f.Close()
        })
    
      #  fileToolStripMenuItem1
      $file_m1.DropDownItems.AddRange(@( $shape_m1, $shape_m2, $dash, $shape_m3))
      $file_m1.Name = "DrawToolStripMenuItem1"
      $file_m1.Text = "Draw"
    
      $m.ResumeLayout($false)
    
      #  MenuTest
      $f.AutoScaleDimensions = New-Object System.Drawing.SizeF (1,1)
      $f.Controls.AddRange(@( $m,$b))
      $f.Topmost = $True
    
      $f.Add_Shown({ $f.Activate() })
    
      [void]$f.ShowDialog([win32window]($caller))
    
      $f.Dispose()
    }

    result

    调用者通过引用传递数据。

    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    $data = @{
      "USA" = 10;
      "UK" = 30;
      "Japan" = 60;
      "China" = 40;
      "Bhutan" = 5;
      "India" = 60; 
    }
    [void](DrawGraph -Title $title -caller $caller -data_ref ([ref]$data))

    数据网格概念验证

    grid

    网格无疑是提供给用户操作的最复杂对象。

    function PromptGrid(
        [System.Collections.IList] $data,
        [Object] $caller = $null
        ){
    
      if ($caller -eq $null ){
        $caller = New-Object Win32Window -ArgumentList
        ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
      }
    
    [System.Reflection.Assembly]::LoadWithPartiaName('System.Windows.Forms') | out-null
    [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel') | out-null
    [System.Reflection.Assembly]::LoadWithPartialName('System.Data') | out-null
    [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')  | out-null
    
    $f = New-Object System.Windows.Forms.Form
    $f.Text = 'how do we open these stones? '
    $f.AutoSize  = $true
    $grid = New-Object System.Windows.Forms.DataGrid
    $grid.PreferredColumnWidth = 100
    
    $System_Drawing_Size = New-Object System.Drawing.Size
    $grid.DataBindings.DefaultDataSourceUpdateMode = 0
    $grid.HeaderForeColor = [System.Drawing.Color]::FromArgb(255,0,0,0)
    
    $grid.Name = "dataGrid1"
    $grid.DataMember = ''
    $grid.TabIndex = 0
    $System_Drawing_Point = New-Object System.Drawing.Point
    $System_Drawing_Point.X = 13;
    $System_Drawing_Point.Y = 48 ;
    $grid.Location = $System_Drawing_Point
    $grid.Dock = [System.Windows.Forms.DockStyle]::Fill
    
    $button = New-Object System.Windows.Forms.Button
    $button.Text = 'Open'
    $button.Dock = [System.Windows.Forms.DockStyle]::Bottom
    
    $f.Controls.Add( $button )
    $f.Controls.Add( $grid )
    
    $button.add_Click({
    # http://msdn.microsoft.com/en-us/library/system.windows.forms.datagridviewrow.cells%28v=vs.110%29.aspx
    
    if ($grid.IsSelected(0)){
     $caller.Data = 42;
    }
    $f.Close()
    
    })
    
    $grid.DataSource =  $data
    $f.ShowDialog([Win32Window ] ($caller)) | out-null
    $f.Topmost = $True
    $f.refresh()
    $f.Dispose()
    }
    
    function display_result{
    param ([Object] $result)
    
    $array = New-Object System.Collections.ArrayList
    
    foreach ($key in $result.keys){
      $value = $result[$key]
      $o = New-Object PSObject
      $o | add-member Noteproperty 'Substance'  $value[0]
      $o | add-member Noteproperty 'Action' $value[1]
      $array.Add($o)
    }
    
    $process_window = New-Object Win32Window -ArgumentList
    ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    $ret = (PromptGrid $array $process_window)
    }
    
    $data = @{ 1 = @('wind', 'blows...');
               2 = @('fire',  'burns...');
               3 = @('water',  'falls...')
            }
    
    display_result $data

    这里,事件处理程序暂时留给读者练习——它可能高度依赖于领域。请访问作者的github 存储库以获取此脚本的更新。

    例如,可以使用 GridListView 提示用户输入缺失的参数。如果脚本参数是

    [CmdletBinding()]param ( [string] $string_param1 = '' ,
            [string] $string_param2 = '' ,
            [string] $string_param3 = '' ,
            [boolean] $boolean_param = $false,
            [int] $int_param
        )

    并且调用仅传递了部分参数,则可以使用以下代码片段来发现参数状态。

    [CmdletBinding()]# Get the command name
    $CommandName = $PSCmdlet.MyInvocation.InvocationName
    
    # Get the list of parameters for the command
    $ParameterList = (Get-Command -Name $CommandName).Parameters
    $parameters = @{}
    foreach ($Parameter in $ParameterList) {
      # Grab each parameter value, using Get-Variable
      $value = Get-Variable -Name $Parameter.Values.Name -ErrorAction SilentlyContinue
    }

    然后填充 $parameters Hashtable 并将其传递给 Form

    $parameters = @{ }
    $value | foreach-object {$parameters[$_.Name] = $_.Value }
    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    Edit_Parameters -parameters ($parameters) -caller $caller -title 'Provide parameters: '

    其定义如下:

    function Edit_Parameters {
         Param(
        [Hashtable] $parameters,
            [String] $title,
        [Object] $caller= $null
        )
    
      [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | out-null
      [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel') | out-null
      [System.Reflection.Assembly]::LoadWithPartialName('System.Data') | out-null
      [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')  | out-null
    
      $f = New-Object System.Windows.Forms.Form
      $f.SuspendLayout();
    
      $f.Text = $title
      $f.AutoSize  = $true
      $grid = New-Object System.Windows.Forms.DataGridView
      $grid.Autosize = $true
      $grid.DataBindings.DefaultDataSourceUpdateMode = 0
    
      $grid.Name = 'dataGrid1'
      $grid.DataMember = ''
      $grid.TabIndex = 0
      $grid.Location = new-object System.Drawing.Point(13,50)
      $grid.Dock = [System.Windows.Forms.DockStyle]::Fill
    
      $grid.ColumnCount = 2
      $grid.Columns[0].Name = 'Parameter Name'
      $grid.Columns[1].Name = 'Value'
    
      $parameters.Keys | foreach-object {
                $row1 = @( $_,  $parameters[$_].ToString())
                $grid.Rows.Add($row1)
      }
    
      $grid.Columns[0].ReadOnly = $true;
    
      foreach ($row in $grid.Rows){
                 $row.cells[0].Style.BackColor = [System.Drawing.Color]::LightGray
                 $row.cells[0].Style.ForeColor = [System.Drawing.Color]::White
                 $row.cells[1].Style.Font = New-Object System.Drawing.Font('Lucida Console', 9)
          }
    
      $button = New-Object System.Windows.Forms.Button
      $button.Text = 'Run'
      $button.Dock = [System.Windows.Forms.DockStyle]::Bottom
    
      $f.Controls.Add( $button)
      $f.Controls.Add( $grid )
      $grid.ResumeLayout($false)
      $f.ResumeLayout($false)
    
      $button.add_Click({
    
        foreach ($row in $grid.Rows){
          # do not close the form if some parameters are not entered
          if (($row.cells[0].Value -ne $null -and $row.cells[0].Value -ne '' ) -and ($row.cells[1].Value -eq $null -or $row.cells[1].Value -eq '')) {
            $row.cells[0].Style.ForeColor = [System.Drawing.Color]::Red
            $grid.CurrentCell  = $row.cells[1]
            return;
          }
        }
          # TODO: return $caller.HashData
          # write-host ( '{0} = {1}' -f $row.cells[0].Value, $row.cells[1].Value.ToString())
    
        $f.Close()
    
      })
    
      $f.ShowDialog($caller) | out-null
      $f.Topmost = $True
      $f.refresh()
      $f.Dispose()
    }

    在按钮处理程序中,我们防止窗体关闭,直到没有空白参数为止。输入焦点会移到期望输入单元格。为简单起见,此处我们接受所有参数的文本输入,而不考虑其类型。

    password

    列表视图

    现在假设您运行一系列松散的(例如 Selenium)测试,这些测试使用 Excel 文件作为测试参数和结果。

    configuration

    读取设置

    $data_name = 'Servers.xls'
    [string]$filename = ('{0}\{1}' -f (Get-ScriptDirectory),$data_name)
    
    $sheet_name = 'ServerList$'
    [string]$oledb_provider = 'Provider=Microsoft.Jet.OLEDB.4.0'
    $data_source = "Data Source = $filename"
    $ext_arg = "Extended Properties=Excel 8.0"
    # TODO: hard coded id
    [string]$query = "Select * from [${sheet_name}] where [id] <> 0"
    [System.Data.OleDb.OleDbConnection]$connection = New-Object System.Data.OleDb.OleDbConnection ("$oledb_provider;$data_source;$ext_arg")
    [System.Data.OleDb.OleDbCommand]$command = New-Object System.Data.OleDb.OleDbCommand ($query)
    
    
    [System.Data.DataTable]$data_table = New-Object System.Data.DataTable
    [System.Data.OleDb.OleDbDataAdapter]$ole_db_adapter = New-Object System.Data.OleDb.OleDbDataAdapter
    $ole_db_adapter.SelectCommand = $command
    
    $command.Connection = $connection
    ($rows = $ole_db_adapter.Fill($data_table)) | Out-Null
    $connection.open()
    $data_reader = $command.ExecuteReader()
    $plain_data = @()
    $row_num = 1
    [System.Data.DataRow]$data_record = $null
    if ($data_table -eq $null) {}
    else {
    
      foreach ($data_record in $data_table) {
        $data_record | Out-Null
        # Reading the columns of the current row
    
        $row_data = @{
          'id' = $null;
          'baseUrl' = $null;
          'status' = $null;
          'date' = $null;
          'result' = $null;
          'guid' = $null;
          'environment' = $null ;
          'testName' = $null;
    
        }
    
        [string[]]($row_data.Keys) | ForEach-Object {
          # An error occurred while enumerating through a collection: Collection was
          # modified; enumeration operation may not execute..
          $cell_name = $_
          $cell_value = $data_record."${cell_name}"
          $row_data[$cell_name] = $cell_value
        }
        Write-Output ("row[{0}]" -f $row_num)
        $row_data
        Write-Output "`n"
        # format needs to be different 
        $plain_data += $row_data
        $row_num++
      }
    }
    
    $data_reader.Close()
    $command.Dispose()
    $connection.Close()

    写入结果

    function update_single_field {
      param(
        [string]$sql,
        # [ref]$connection does not seem to work here
        # [System.Management.Automation.PSReference]$connection_ref,
        [System.Data.OleDb.OleDbConnection]$connection,
        [string]$where_column_name,
        [object]$where_column_value,
        [string]$update_column_name,
        [object]$update_column_value,
        [System.Management.Automation.PSReference]$update_column_type_ref = ([ref][System.Data.OleDb.OleDbType]::VarChar),
        [System.Management.Automation.PSReference]$where_column_type_ref = ([ref][System.Data.OleDb.OleDbType]::Numeric)
      )
    
      [System.Data.OleDb.OleDbCommand]$local:command = New-Object System.Data.OleDb.OleDbCommand
      $local:command.Connection = $connection
    
      $local:command.Parameters.Add($update_column_name,$update_column_type_ref.Value).Value = $update_column_value
      $local:command.Parameters.Add($where_column_name,$where_column_type_ref.Value).Value = $where_column_value
      $local:command.CommandText = $sql
    
      # TODO: Exception calling "Prepare" with "0" argument(s): "OleDbCommand.Prepare method requires all variable length parameters to have an explicitly set non-zero Size."
      # $command.Prepare()
    
      $local:result = $local:command.ExecuteNonQuery()
      Write-Output ('Update query: {0}' -f (($sql -replace $update_column_name,$update_column_value) -replace $where_column_name,$where_column_value))
      Write-Output ('Update result: {0}' -f $local:result)
    
      $local:command.Dispose()
    
      return $local:result
    
    }
    
    update_single_field `
       -connection $connection `
       -sql "UPDATE [${sheet_name}] SET [status] = @status WHERE [id] = @id" `
       -update_column_name "@status" `
       -update_column_value $false `
       -update_column_type_ref ([ref][System.Data.OleDb.OleDbType]::Boolean) `
       -where_column_name '@id' `
       -where_column_value 2

    一些自定义函数被编写。测试框(例如 Spoon.Net)上可能没有安装 Excel,并且当测试数量增加时,选择特定测试重新运行并不方便。网格视图可以有所帮助(可以说是这只是一个初步的解决方案,可能存在更好的解决方案)。

    $RESULT_OK = 0
    $RESULT_CANCEL = 2
    $Readable = @{
      $RESULT_OK = 'OK'
      $RESULT_CANCEL = 'CANCEL'
    }
    
    
    # http://www.cosmonautdreams.com/2013/09/06/Parse-Excel-Quickly-With-Powershell.html
    # for singlee column spreadsheets see also
    # http://blogs.technet.com/b/heyscriptingguy/archive/2008/09/11/how-can-i-read-from-excel-without-using-excel.aspx
    
    function PromptGrid (
      [System.Collections.IList]$data,
      [object]$caller = $null
    ) {
    
      if ($caller -eq $null) {
        $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
      }
    
      [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null
      [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel') | Out-Null
      [System.Reflection.Assembly]::LoadWithPartialName('System.Data') | Out-Null
      [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | Out-Null
    
    
      $f = New-Object System.Windows.Forms.Form
      $f.Text = 'Test suite'
      $f.AutoSize = $true
      $grid = New-Object System.Windows.Forms.DataGrid
      $grid.PreferredColumnWidth = 100
    
      $System_Drawing_Size = New-Object System.Drawing.Size
      $grid.DataBindings.DefaultDataSourceUpdateMode = 0
      $grid.HeaderForeColor = [System.Drawing.Color]::FromArgb(255,0,0,0)
    
      $grid.Name = 'dataGrid1'
      $grid.DataMember = ''
      $grid.TabIndex = 0
      $System_Drawing_Point = New-Object System.Drawing.Point
      $System_Drawing_Point.X = 13;
      $System_Drawing_Point.Y = 48;
      $grid.Location = $System_Drawing_Point
      $grid.Dock = [System.Windows.Forms.DockStyle]::Fill
    
      $button = New-Object System.Windows.Forms.Button
      $button.Text = 'Open'
      $button.Dock = [System.Windows.Forms.DockStyle]::Bottom
    
      $f.Controls.Add($button)
      $f.Controls.Add($grid)
      $button.add_click({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
          # http://msdn.microsoft.com/en-us/library/system.windows.forms.datagridviewrow.cells%28v=vs.110%29.aspx
          # TODO:
          # [System.Windows.Forms.DataGridViewSelectedRowCollection]$rows = $grid.SelectedRows
          # [System.Windows.Forms.DataGridViewRow]$row = $null 
          # [System.Windows.Forms.DataGridViewSelectedCellCollection] $selected_cells = $grid.SelectedCells; 
          $script:Data = 0
          $script:Status = $RESULT_CANCEL
          # $last_row = ($grid.Rows.Count)
          $last_row =  $data.Count
          for ($counter = 0; $counter -lt $last_row;$counter++) { 
            if ($grid.IsSelected($counter)) {
              $row =  $data[$counter]
              $script:Data = $row.Guid
              $script:Status = $RESULT_OK
             }
          }
          $f.Close()
    
        })
    
      $grid.DataSource = $data
      $f.ShowDialog() | Out-Null
      $f.Topmost = $True
      $f.Refresh()
    
    }
    
    
    function display_result {
      param([object[]]$result)
      $script:Data = 0 
      $array = New-Object System.Collections.ArrayList
      foreach ($row_data in $result) {
        $o = New-Object PSObject
        foreach ($row_data_key in $row_data.Keys) {
          $row_data_value = $row_data[$row_data_key]
    
          $o | Add-Member Noteproperty $row_data_key $row_data_value
        }
        [void]$array.Add($o)
      }
    
      $process_window = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
      $ret = (PromptGrid $array $process_window)
      if ($script:Status -eq $RESULT_OK ) { 
        Write-Output @( 'Rerun ->', $script:Data )
      }
    }

    gridview

    完整的脚本源代码可在源 zip 文件中找到。

    纯 ListView 容器的渲染方式如下:

    function PromptListView
    {
      param(
        [System.Collections.IList]$data_rows,
        [string[]]$column_names = $null,
        [string[]]$column_tags,
        [bool]$debug
      )
      @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
    
      $numCols = $column_names.Count
    
      # figure out form width
      $width = $numCols * 120
    
      $title = 'Select process'
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
      $f.Size = New-Object System.Drawing.Size ($width,400)
      $f.StartPosition = 'CenterScreen'
    
      $f.KeyPreview = $true
    
      $select_button = New-Object System.Windows.Forms.Button
      $select_button.Location = New-Object System.Drawing.Size (10,10)
      $select_button.Size = New-Object System.Drawing.Size (70,23)
      $select_button.Text = 'Select'
      $select_button.add_click({
          # TODO: implementation 
          # select_sailing ($script:Item)
        })
    
      $button_panel = New-Object System.Windows.Forms.Panel
      $button_panel.Height = 40
      $button_panel.Dock = 'Bottom'
      $button_panel.Controls.AddRange(@( $select_button))
    
    
      $panel = New-Object System.Windows.Forms.Panel
      $panel.Dock = 'Fill'
      $f.Controls.Add($panel)
      $list_view = New-Object windows.forms.ListView
      $panel.Controls.AddRange(@( $list_view,$button_panel))
    
    
      # create the columns
      $list_view.View = [System.Windows.Forms.View]'Details'
      $list_view.Size = New-Object System.Drawing.Size ($width,350)
      $list_view.FullRowSelect = $true
      $list_view.GridLines = $true
      $list_view.Dock = 'Fill'
      foreach ($col in $column_names) {
        [void]$list_view.Columns.Add($col,100)
      }
    
      # populate the view
      foreach ($data_row in $data_rows) {
        # NOTE: special processing of first column
        $cell = (Invoke-Expression (('$data_row.{0}' -f $column_names[0]))).ToString()
        $item = New-Object System.Windows.Forms.ListViewItem ($cell)
        for ($i = 1; $i -lt $column_names.Count; $i++) {
          $cell = (Invoke-Expression ('$data_row.{0}' -f $column_names[$i]))
    
          if ($cell -eq $null) {
            $cell = ''
          }
          [void]$item.SubItems.Add($cell.ToString())
        }
        $item.Tag = $data_row
        [void]$list_view.Items.Add($item)
      }
      <#
      $list_view.add_ItemActivate({
          param(
            [object]$sender,[System.EventArgs]$e)
    
          [System.Windows.Forms.ListView]$lw = [System.Windows.Forms.ListView]$sender
          [string]$filename = $lw.SelectedItems[0].Tag.ToString()
        })
      #>
    
      # store the selected item id
      $list_view.add_ItemSelectionChanged({
          param(
            [object]$sender,[System.Windows.Forms.ListViewItemSelectionChangedEventArgs]$e)
    
          [System.Windows.Forms.ListView]$lw = [System.Windows.Forms.ListView]$sender
          [int]$process_id = 0
          [int32]::TryParse(($e.Item.SubItems[0]).Text,([ref]$process_id))
          $script:Item = $process_id
          # write-host ( '-> {0}' -f $script:Item )
        })
    
      # tags for sorting
      for ($i = 0; $i -lt $column_tags.Count; $i++) {
        $list_view.Columns[$i].Tag = $column_tags[$i]
      }
      # see below..
      $list_view.Add_ColumnClick({
          $list_view.ListViewItemSorter = New-Object ListViewItemComparer ($_.Column,$script:IsAscending)
          $script:IsAscending = !$script:IsAscending
        })
      $script:Item = 0
      $script:IsAscending = $false
      $f.Topmost = $True
      $script:IsAscending = $false
      $f.Add_Shown({ $f.Activate() })
      $x = $f.ShowDialog()
    }
    
    

    带有排序

    using System;
    using System.Windows.Forms;
    using System.Drawing;
    using System.Collections;
    
    public class ListViewItemComparer : System.Collections.IComparer
    {
        public int col = 0;
        public System.Windows.Forms.SortOrder Order;
        public ListViewItemComparer()
        {
            col = 0;
        }
    
        public ListViewItemComparer(int column, bool asc)
        {
            col = column;
            if (asc)
            { Order = SortOrder.Ascending; }
            else
            { Order = SortOrder.Descending; }
        }
    
        public int Compare(object x, object y)
        {
            if (!(x is ListViewItem)) return (0);
            if (!(y is ListViewItem)) return (0);
    
            ListViewItem l1 = (ListViewItem)x;
            ListViewItem l2 = (ListViewItem)y;
    
            if (l1.ListView.Columns[col].Tag == null)
            {
                l1.ListView.Columns[col].Tag = "Text";
            }
    
            if (l1.ListView.Columns[col].Tag.ToString() == "Numeric")
            {
                float fl1 = float.Parse(l1.SubItems[col].Text);
                float fl2 = float.Parse(l2.SubItems[col].Text);
                return (Order == SortOrder.Ascending) ?  fl1.CompareTo(fl2) :  fl2.CompareTo(fl1);
            }
            else
            {
                string str1 = l1.SubItems[col].Text;
                string str2 = l2.SubItems[col].Text;
                return (Order == SortOrder.Ascending) ? str1.CompareTo(str2) : str2.CompareTo(str1);
            }
    
        }
    }
    
    

    listview

    function display_result {
      param([object[]]$result)
      $column_names = @(
        'id',
        'dest',
        'port',
        'state',
        'title',
        'link'
      )
      $column_tags = @(
        'Numeric',
        'Text',
        'Text',
        'Text',
        'Text',
        'Text'
    
      )
      $data_rows = New-Object System.Collections.ArrayList
      foreach ($row_data in $result) {
        $o = New-Object PSObject
        foreach ($row_data_key in $column_names) {
          $row_data_value = $row_data[$row_data_key]
          $o | Add-Member Noteproperty $row_data_key $row_data_value
        }
        [void]$data_rows.Add($o)
      }
    
      [void](PromptListView -data_rows $data_rows -column_names $column_names -column_tags $column_tags)
    }
    
    

    填充 GridView DataTable

    一次将数据加载到网格或列表视图中可能不是理想的界面。通用的字典列表似乎不起作用,作为一种解决方法,您可以将其存储在合适的类中。

        public class DictionaryContainer
        {
            private List<Dictionary<string, object>> _data = new List<Dictionary<string, object>> { };
    
            public List<Dictionary<string, object>> Data
            {
                get { return _data; }
            }
    
            public void add_row(Dictionary<string, object> row)
            {
                _data.Add(row);
            }
    
            public DictionaryContainer()
            {
            }
        }
    

    在此示例中,使用了带有切换所有复选框的 DataGridView 类来渲染数据。

    function SelectAllGrid {
    
      param(
        [string]$title,
        [string]$message
      )
    
    
      @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
    
      $f.Size = New-Object System.Drawing.Size (470,235)
      $f.AutoScaleDimensions = New-Object System.Drawing.SizeF (6.0,13.0)
      $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
      $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
      $f.StartPosition = 'CenterScreen'
    
      $urls = @( 'http://www.travelocity.com/','http://www.bcdtravel.com/','http://www.airbnb.com','http://www.priceline.com','http://www.tripadvisor.com')
    
    
      # https://groups.google.com/forum/#!topic/microsoft.public.windows.powershell/Ta9NyFPovgI 
      $array_of_dictionaries_container = New-Object -Type 'Custom.DictionaryContainer'
    
      for ($cnt = 0; $cnt -ne 5; $cnt++) {
        $item = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
        $item.Add('RandomNo',(Get-Random -Minimum 1 -Maximum 10001))
        $item.Add('date',(Date))
        $item.Add('url',$urls[$cnt])
        $array_of_dictionaries_container.add_row($item)
      }
    
      $r = New-Object -TypeName 'Custom.SelectAllGrid' -ArgumentList $array_of_dictionaries_container
      $r.Size = $f.Size
    
      $f.Controls.Add($r)
      $f.Topmost = $True
    
      $f.Add_Shown({ $f.Activate() })
    
      [void]$f.ShowDialog()
      $f.Dispose()
    
    }
    $script:Data = $null
    SelectAllGrid -Title 'Selection Grid Sample Project'
    
    

    它被修改为 Panel 而不是 Form,并接受

            private System.Windows.Forms.DataGridView dgvSelectAll;
            public SelectAllGrid(DictionaryContainer userDataContainer = null)
            {
               this.dgvSelectAll = new System.Windows.Forms.DataGridView();
               // ... misc initialization code 
               dgvSelectAll.DataSource = GetDataSource(userDataContainer);
            }
            public DataTable GetDataSource(DictionaryContainer userDataContainer = null)
            {
                DataTable dTable = new DataTable();
    
                DataRow dRow = null;
                List> sampleData;
                if (userDataContainer == null)
                {
                    Random rnd = new Random();
                    sampleData = new List> {
    
                                  new Dictionary { { "RandomNo", rnd.NextDouble()}, { "Date", DateTime.Now.ToString("MM/dd/yyyy") }, { "url", "www.facebook.com"}} ,
                                  new Dictionary { { "RandomNo", rnd.NextDouble()}, { "Date", DateTime.Now.ToString("MM/dd/yyyy") }, { "url", "www.linkedin.com"}} ,
                                  new Dictionary { { "RandomNo", rnd.NextDouble()}, { "Date", DateTime.Now.ToString("MM/dd/yyyy") }, { "url", "www.odesk.com"}}  
                                  };
                }
                else
                {
                    sampleData = userDataContainer.Data;
                }
                Dictionary openWith = sampleData[0];
    
                Dictionary.KeyCollection keyColl = openWith.Keys;
    
                dTable.Columns.Add("IsChecked", System.Type.GetType("System.Boolean"));
                foreach (string s in keyColl)
                {
                    dTable.Columns.Add(s);
                }
    
                foreach (Dictionary objitem in sampleData)
                {
                    dRow = dTable.NewRow();
                    foreach (KeyValuePair kvp in objitem)
                    {
                        dRow[kvp.Key] = kvp.Value.ToString();
                    }
                    dTable.Rows.Add(dRow);
                    dTable.AcceptChanges();
    
                }
                return dTable;
            }
    
    

    password

    请注意,修改 SelectAllGrid 以直接接受 List<Dictionary<string, object>> 并通过以下方式传递数据

    $array_of_dictionaries = New-Object 'System.Collections.Generic.List[System.Collections.Generic.Dictionary[String,Object]]'
    for ($cnt = 0; $cnt -ne 5; $cnt++) {
        $item = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
        $item.Add('RandomNo',(Get-Random -Minimum 1 -Maximum 10001))
        $item.Add('date',(Date))
        $item.Add('url',$urls[$cnt])
        $array_of_dictionaries.Add($item)
      }
      $array_of_dictionaries | ForEach-Object { $row = $_
        $row | Format-List
      }
     $r = New-Object -TypeName 'Custom.SelectAllGrid' -ArgumentList $array_of_dictionaries
    

    会导致错误

    New-Object : Cannot find an overload for "SelectAllGrid" and the argument count: "5".
    

    并且必须将 System.Data.dll 添加到 Custom.SelectAllGrid 的引用程序集列表中以防止错误。

    Add-Type : c:\Documents and Settings\Administrator\Local Settings\Temp\ypffadcb.0.cs(90) : 
    The type 'System.Xml.Serialization.IXmlSerializable' is defined in an assembly that is not referenced. 
    You must add a reference to assembly 
    'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.
    

    带可折叠组的列表

    下一个示例使用可折叠组控件向用户提供聚合配置信息。

    function GroupedListBox
    {
      param(
        [string]$title,
     [bool]$show_buttons)
    
    
      @('System.Drawing','System.Collections', 'System.Collections.Generic' , 'System.Drawing', 'System.ComponentModel', 'System.Windows.Forms', 'System.Data') |  foreach-object {   [void] [System.Reflection.Assembly]::LoadWithPartialName($_) } 
    
      $f = New-Object System.Windows.Forms.Form
    
      $f.Text = $title
      $width = 500
      $f.Size = New-Object System.Drawing.Size ($width,400)
      $glc = New-Object -TypeName 'GroupedListControl.GroupListControl'
      $glc.SuspendLayout()
    
      $glc.AutoScroll = $true
      $glc.BackColor = [System.Drawing.SystemColors]::Control
      $glc.FlowDirection = [System.Windows.Forms.FlowDirection]::TopDown
      $glc.SingleItemOnlyExpansion = $false
      $glc.WrapContents = $false
      $glc.Anchor = ([System.Windows.Forms.AnchorStyles](0 `
             -bor [System.Windows.Forms.AnchorStyles]::Top `
             -bor [System.Windows.Forms.AnchorStyles]::Bottom `
             -bor [System.Windows.Forms.AnchorStyles]::Left `
             -bor [System.Windows.Forms.AnchorStyles]::Right `
          ))
    
      $f.SuspendLayout()
    
      if ($show_buttons) {
            [System.Windows.Forms.CheckBox]$cb1 = new-object -TypeName 'System.Windows.Forms.CheckBox'
            $cb1.AutoSize = $true
            $cb1.Location = new-object System.Drawing.Point(12, 52)
            $cb1.Name = "chkSingleItemOnlyMode"
            $cb1.Size = new-object System.Drawing.Size(224, 17)
            $cb1.Text = 'Single-Group toggle'
            $cb1.UseVisualStyleBackColor = $true
            function chkSingleItemOnlyMode_CheckedChanged
            {
             param([Object] $sender, [EventArgs] $e)
                $glc.SingleItemOnlyExpansion = $cb1.Checked
                if ($glc.SingleItemOnlyExpansion) {
                    $glc.CollapseAll()
                } else {
                    $glc.ExpandAll()
                }
            }
            $cb1.Add_CheckedChanged({ chkSingleItemOnlyMode_CheckedChanged } )
            [System.Windows.Forms.Label]$label1 = new-object -TypeName 'System.Windows.Forms.Label' 
            $label1.Location = new-object System.Drawing.Point(12, 13)
            $label1.Size = new-object System.Drawing.Size(230, 18) 
    	$label1.Text = 'Grouped List Control Demo'
            # $label1.Font = new System.Drawing.Font("Lucida Sans", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)))
            [System.Windows.Forms.Button]$button1 = new-object -TypeName 'System.Windows.Forms.Button' 
    
                $button1.Location = new-object System.Drawing.Point(303, 46)
                $button1.Name = "button1"
                $button1.Size = new-object System.Drawing.Size(166, 23)
                $button1.TabIndex = 3
                $button1.Text = 'Add Data Items (disconnected)'
                $button1.UseVisualStyleBackColor = true
                $button1.Add_Click( { write-host $glc.GetType() 
    $x =  $glc | get-member
    write-host ($x -join "`n")
    })
             
        $f.Controls.Add($cb1)
        $f.Controls.Add($button1)
        $f.Controls.Add($label1)
    
        $glc.Location =  new-object System.Drawing.Point(0, 75)
        $glc.Size =  new-object  System.Drawing.Size($f.size.Width, ($f.size.Height - 75))
    
      } else { 
      $glc.Size = $f.Size
    
    }
    
      for ($group = 1; $group -le 5; $group++)
      {
        [GroupedListControl.ListGroup]$lg = New-Object -TypeName 'GroupedListControl.ListGroup'
        $lg.Columns.Add("List Group " + $group.ToString(), 120 )
        $lg.Columns.Add("Group " + $group + " SubItem 1", 150 )
        $lg.Columns.Add("Group " + $group + " Subitem 2", 150 )
        $lg.Name = ("Group " + $group)
        # add some sample items:
        for ($j = 1; $j -le 5; $j++){
          [System.Windows.Forms.ListViewItem]$item = $lg.Items.Add(("Item " + $j.ToString()))
          $item.SubItems.Add($item.Text + " SubItem 1")
          $item.SubItems.Add($item.Text + " SubItem 2")
        }
    
        $glc.Controls.Add($lg)
      }
    
      $f.Controls.Add($glc)
      $glc.ResumeLayout($false)
    
      $f.ResumeLayout($false)
    
      $f.StartPosition = 'CenterScreen'
    
      $f.KeyPreview = $True
    
      $f.Topmost = $True
    
      $caller = New-Object -TypeName 'Win32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    
      $f.Add_Shown({ $f.Activate() })
    
      [void]$f.ShowDialog([win32window]($caller))
      $f.Dispose()
      $result = $caller.Message
      $caller = $null
      return $result
    }
    $show_buttons_arg  = $false
    
      if ($PSBoundParameters["show_buttons"]) {
    $show_buttons_arg = $true
      }

    要传递要显示的数据,请使用以下结构:

    $configuration_discovery_results = @{
      'Web.config' = @{
        'COMMENT' = 'Web Server';
        'DOMAIN' = '';
        'CONFIGURATIONS' = @{
          'Exit SSL cms targetted offers' = $Extract_appSetting;
          'Force Non Https for Home Page' = $Extract_appSetting;
          'To new deck plans page' = $Extract_RuleActionurl ;
          'imagesCdnHostToPrepend' = $Extract_RuleActionurl ;
        };
      };
    
    [scriptblock]$Extract_appSetting = {
      param(
        [System.Management.Automation.PSReference]$object_ref,
        [System.Management.Automation.PSReference]$result_ref,
        [string]$key = $null
      )
    
      if ($key -eq $null -or $key -eq '') {
        throw 'Key cannot be null'
    
      }
    
    [scriptblock]$Extract_RuleActionurl = {
      param(
        [System.Management.Automation.PSReference]$object_ref,
        [System.Management.Automation.PSReference]$result_ref,
        [string]$key = $null
      )
    
      if ($key -eq $null -or $key -eq '') {
        throw 'Key cannot be null'
      }
    
      $data = @{}
      $nodes = $object_ref.Value.Configuration.Location.'system.webServer'.rewrite.rules.rule
      if ($global:debug) {
        Write-Host $nodes.count
      }
      for ($cnt = 0; $cnt -ne $nodes.count; $cnt++) {
    
        $k = $nodes[$cnt].Getattribute('name')
        $v = $nodes[$cnt].action.Getattribute('url')
        if ($k -match $key) {
          $data[$k] += $v
          if ($global:debug) {
            Write-Output $k; Write-Output $v
          }
        }
    
      }
    
      $result_ref.Value = $data[$key]
    }
    
      $data = @{}
      $nodes = $object_ref.Value.Configuration.Location.appSettings.Add
      for ($cnt = 0; $cnt -ne $nodes.count; $cnt++) {
        $k = $nodes[$cnt].Getattribute('key')
        $v = $nodes[$cnt].Getattribute('value')
    
        if ($k -match $key) {
          if ($global:debug) {
            Write-Host $k
            Write-Host $key
            Write-Host $v
          }
          $data[$k] += $v
        }
      }
      $result_ref.Value = $data[$key]
    }

    要从各种 *.config 文件收集数据,请使用例如代码:

    function collect_config_data {
    
      param(
        [ValidateNotNull()]
        [string]$target_domain,
        [string]$target_unc_path,
        [scriptblock]$script_block,
        [bool]$verbose,
        [bool]$debug
      )
    
      $local:result = @()
      if (($target_domain -eq $null) -or ($target_domain -eq '')) {
        if ($powerless) {
          return $local:result
        } else {
          throw 'unspecified DOMAIN'
        }
      }
    
      [xml]$xml_config = Get-Content -Path $target_unc_path
      $object_ref = ([ref]$xml_config)
      $result_ref = ([ref]$local:result)
    
      Invoke-Command $script_block -ArgumentList $object_ref,$result_ref,$verbose,$debug
    
      if ($verbose) {
        Write-Host ("Result:`r`n---`r`n{0}`r`n---`r`n" -f ($local:result -join "`r`n"))
      }
    
    }

    要填充列表,请使用:

      foreach ($key in $configuration_discovery_results.Keys) {
        $values = $configuration_discovery_results[$key]
        $configurations = $values['CONFIGURATIONS']
        [GroupedListControl.ListGroup]$lg = New-Object -TypeName 'GroupedListControl.ListGroup'
        $lg.Columns.Add($values['COMMENT'],120)
        $lg.Columns.Add("Key",150)
        $lg.Columns.Add("Value",300)
        # TODO - document the error.
        #    $configurations.Keys | foreach-object {  
        foreach ($k in $configurations.Keys) {
          $v = $configurations[$k]
          [System.Windows.Forms.ListViewItem]$item = $lg.Items.Add($key)
          $item.SubItems.Add($k)
          $item.SubItems.Add($v)
        }
    
        $glc.Controls.Add($lg)
    
      }

    password

    拖放

    下一个示例涵盖了拖放列表框。有大量的事件需要处理,并且将 MSDN 示例 http://msdn.microsoft.com/en-us/library/system.windows.forms.control.dodragdrop%28v=vs.100%29.aspx 从 C# 转换为 PowerShell 语法是极其冗余且容易出错的。您只需要最终的 ListDragTarget.Items,因此您可以向 Add-Type 添加一个字符串 getter 方法,让其余部分保持不变,去掉主入口点。

      public class DragNDrop : System.Windows.Forms.Panel
      {
    
      private string _message;
      public string Message
       {
          get {
                    _message = "";
                    List<string>  _items = new List<string>();
                    foreach (object _item in ListDragTarget.Items) {
                       _items.Add(_item.ToString());
                    }
                    _message = String.Join(",", _items.ToArray() );
                    return _message;
             }
          set { _message = value; }
    
            private System.Windows.Forms.ListBox ListDragSource;
            private System.Windows.Forms.ListBox ListDragTarget;
            private System.Windows.Forms.CheckBox UseCustomCursorsCheck;
            private System.Windows.Forms.Label DropLocationLabel;
    
            private int indexOfItemUnderMouseToDrag;
            private int indexOfItemUnderMouseToDrop;
    
            private Rectangle dragBoxFromMouseDown;
            private Point screenOffset;
    
            private Cursor MyNoDropCursor;
            private Cursor MyNormalCursor;
    
            /// The main entry point for the application removed.
    
            public DragNDrop(String message)
            {
    
    // rest of the code see http://msdn.microsoft.com/en-us/library/system.windows.forms.control.dodragdrop%28v=vs.100%29.aspx

    并将构造函数更改为接受 String message。此外,在使 DragNDrop 类继承自 System.Windows.Forms.Panel 而不是 System.Windows.Forms.Form 后,它将被放置在窗体上。

    function PromptWithDragDropNish {
    param
    (
    
    [String] $title,
            [Object] $caller
    )
    
    [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
    [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
    
    
    $f = New-Object System.Windows.Forms.Form
    $f.Text = $title
    
    $panel = New-Object DragNDrop($caller.Message)
    
    $f.ClientSize = new-object  System.Drawing.Size(288, 248)
    $f.Controls.AddRange(@( $panel ))
    $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog

    您可以使用 $caller 对象来处理这里的 Message,同时考虑潜在的附加功能,尽管它不是严格必需的。最后,脚本接收结果。

      $f.Add_Shown( { $f.Activate() } )
    
      [Void] $f.ShowDialog([Win32Window ] ($caller) )
      $result = $panel.Message
      $panel.Dispose()
      $f.Dispose()
    
      $caller = $null
    
      return $result
    
    }
    
    $data = @(
       'one','two','three','four','five',
       'six','seven','nine','ten','eleven'
    )
    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    
    $caller.Message = $data -join ','
    $result = PromptWithDragDropNish 'Items'  $caller
    
    # write-debug ('Selection is : {0}' -f  , $result )
    
    $result -split ',' | format-table -autosize

    grid

    窗体相应地调整了光标——这在屏幕截图中未显示。窗体关闭后,脚本会打印选定的项目。这样的控件可能很有用,例如安排 Selenium 测试到子集中(转换为和从 *.orderedtests 资源未显示)。完整的脚本源代码可在源 zip 文件中找到。

    grid

    DF5B1F66EB484A2E8DDC06BD183B0E3F

    上下

    对于时间间隔选择,可以使用 DateTimePicker 和合适的 System.Windows.Forms.DateTimePickerFormat

    或者甚至是 DomainUpDown 派生的自定义时间选择器类

    // http://stackoverflow.com/questions/16789399/looking-for-time-picker-control-with-half-hourly-up-down
    public class CustomTimePicker : System.Windows.Forms.DomainUpDown
        public CustomTimePicker()
        {         
            // build the list of times...
            for (double time = 23.5; time >= 0; time -= 0.5)
            {
                int hour = (int)time; 
                int minutes = (int)((time - hour) * 60); 
                this.Items.Add(hour.ToString("00") + ":" + minutes.ToString("00")); 
            }
            this.SelectedIndex = Items.IndexOf("09:00"); // select a default time
            this.Wrap = true; 
        }

    updown

    $form_onload = {
       $script:numeric_value = 0
       $script:time_value = ''
       $script:custom_value= ''
    function UpDownsPrompt
      param(
        [object]$caller
      )
      @( 'System.Drawing',
        'System.Collections.Generic',
        'System.Collections',
        'System.ComponentModel',
        'System.Windows.Forms',
        'System.Text',
        'System.Data'
      ) | ForEach-Object { $assembly = $_; [void][System.Reflection.Assembly]::LoadWithPartialName($assembly) }
      $f = New-Object System.Windows.Forms.Form
      $f.Size = New-Object System.Drawing.Size (180,120)
      $n = New-Object System.Windows.Forms.NumericUpDown
      $n.SuspendLayout()
      $n.Parent = $this
      $n.Location = New-Object System.Drawing.Point (30,80)
      $n.Size = New-Object System.Drawing.Size (50,20)
      $n.Value = 1
      $n.Minimum = 0
      $n.Maximum = 1000
      $n.Increment = 1
      $n.DecimalPlaces = 0
      $n.ReadOnly = $false
      $n.TextAlign = [System.Windows.Forms.HorizontalAlignment]::Right
      ($n.add_ValueChanged).Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          $script:numeric_value = $n.Value
        }
      )
      $c = New-Object CustomTimePicker
      $c.Parent = $f
      $c.Location = New-Object System.Drawing.Point (30,50)
      $c.Size = New-Object System.Drawing.Size (70,20)
      $c.TextAlign = [System.Windows.Forms.HorizontalAlignment]::Left
      $c.ReadOnly = $true
      ($c.add_TextChanged).Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
            $script:custom_value = $c.SelectedItem.ToString()
        }
      )
      $c.SuspendLayout()
      $c.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
      $c.ReadOnly = $true
      $c.TabIndex = 0
      $c.TabStop = $false
      $s = New-Object System.Windows.Forms.DateTimePicker
      $s.Parent = $f
      $s.Location = New-Object System.Drawing.Point (30,20)
      $s.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
      $s.Size = New-Object System.Drawing.Size (70,20)
      $s.Format = [System.Windows.Forms.DateTimePickerFormat]::Custom
      $s.CustomFormat = 'hh:mm'
      $s.ShowUpDown = $true
      $s.Checked = $false
      $s.Add_VisibleChanged({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs)
          $script:datetime_value = $s.Value
        })
      $f.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
      $f.ClientSize = New-Object System.Drawing.Size (180,120)
      $components = New-Object System.ComponentModel.Container
      $f.Controls.AddRange(@( $c,$n,$s))
      $f.Name = 'Form1'
      $f.Text = 'UpDown Sample'
      $c.ResumeLayout($false)
      $n.ResumeLayout($false)
      $f.ResumeLayout($false)
      $f.StartPosition = 'CenterScreen'
      $f.KeyPreview = $True
      $f.Topmost = $True
      $f.Add_Shown({ $f.Activate() })
      [void]$f.ShowDialog()
      $f.add_Load($form_onload)
      $f.Dispose()
    $DebugPreference = 'Continue'
    Write-Debug ('Time Selection is : {0}' -f $script:datetime_value )
    Write-Debug ('Numeric Value is : {0}' -f $script:numeric_value)
    Write-Debug ('Custom contol Value is : {0}' -f $script:custom_value)

    results

    功能区按钮

    您可以改编 C#.NET 中的浮动/滑动/移动菜单 来仅包含功能区滑块控件和定时器,同时将 UserControl1 的定义通过继承 Panel(原始 Form1)而不是 Form 并移除默认构造函数来移至 PowerShell。

    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Data;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    
    namespace Ribbon
    {
    
        public class Panel : System.Windows.Forms.Panel
        {
    
            private System.Windows.Forms.Panel panel1;
            private System.Windows.Forms.Panel panel2;
            private System.Windows.Forms.Button button2;
            private System.Windows.Forms.Button button1;
            private System.Windows.Forms.Panel panel3;
            private System.Windows.Forms.Timer timer1;
            private System.Windows.Forms.Timer timer2;
            private System.Windows.Forms.UserControl _usrCtrl;
            private System.ComponentModel.IContainer components = null;
      // ...
            public Panel(System.Windows.Forms.UserControl u)
            {
             if (u == null)
                throw new ArgumentNullException("Usercontrol required");
              this._usrCtrl = u; 
              InitializeComponent();
            }

    然后在 PowerShell 语义中设计所有按钮和子面板。

    function PromptRibbon {
    
      param(
        [string]$title,
        [string]$message,
        [object]$caller
      )
    
    
      @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
    
      $f.Size = New-Object System.Drawing.Size (470,135)
      $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
      $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
      $f.StartPosition = 'CenterScreen'
    
      $u = New-Object System.Windows.Forms.UserControl
      $p1 = New-Object System.Windows.Forms.Panel
      $l1 = New-Object System.Windows.Forms.Label
    
      $p2 = New-Object System.Windows.Forms.Panel
      $l2 = New-Object System.Windows.Forms.Label
    
      $b1 = New-Object System.Windows.Forms.Button
      $b2 = New-Object System.Windows.Forms.Button
      $b3 = New-Object System.Windows.Forms.Button
      $b4 = New-Object System.Windows.Forms.Button
      $b5 = New-Object System.Windows.Forms.Button
      $b6 = New-Object System.Windows.Forms.Button
      $b7 = New-Object System.Windows.Forms.Button
      $b8 = New-Object System.Windows.Forms.Button
      $b9 = New-Object System.Windows.Forms.Button
      $b10 = New-Object System.Windows.Forms.Button
      $b11 = New-Object System.Windows.Forms.Button
      $b12 = New-Object System.Windows.Forms.Button
      $b13 = New-Object System.Windows.Forms.Button
      $b14 = New-Object System.Windows.Forms.Button
      $b15 = New-Object System.Windows.Forms.Button
      $b16 = New-Object System.Windows.Forms.Button
      $b17 = New-Object System.Windows.Forms.Button
      $b18 = New-Object System.Windows.Forms.Button
      $b19 = New-Object System.Windows.Forms.Button
      $b20 = New-Object System.Windows.Forms.Button
      $p1.SuspendLayout()
      $p2.SuspendLayout()
      $u.SuspendLayout()
    
      function button_click {
        param(
          [object]$sender,
          [System.EventArgs]$eventargs
        )
        $who = $sender.Text
        [System.Windows.Forms.MessageBox]::Show(("We are processing {0}.`rThere is no callback defined yet." -f $who))
      }
    
      $callbacks = @{
        'b1' = [scriptblock]{
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          $who = $sender.Text
          [System.Windows.Forms.MessageBox]::Show(("We are processing`rcallback function for {0}." -f $who))
        };
        'b3' = [scriptblock]{
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          $who = $sender.Text
          [System.Windows.Forms.MessageBox]::Show(("We are processing`rcallback function defined for {0}." -f $who))
        };
    
      }
    
      #  panels
      $cnt = 0
      @(
        ([ref]$p1),
        ([ref]$p2)
      ) | ForEach-Object {
        $p = $_.Value
        $p.BackColor = [System.Drawing.Color]::Silver
        $p.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
        $p.Dock = [System.Windows.Forms.DockStyle]::Left
        $p.Location = New-Object System.Drawing.Point ((440 * $cnt),0)
        $p.Name = ('panel {0}' -f $cnt)
        $p.Size = New-Object System.Drawing.Size (440,100)
        $p.TabIndex = $cnt
        $cnt++
      }
    
      # labels
      $cnt = 0
      @(
        ([ref]$l1),
        ([ref]$l2)
      ) | ForEach-Object {
        $l = $_.Value
        $l.BackColor = [System.Drawing.Color]::DarkGray
        $l.Dock = [System.Windows.Forms.DockStyle]::Top
        $l.Location = New-Object System.Drawing.Point (0,0)
        $l.Name = ('label {0}' -f $cnt)
        $l.Size = New-Object System.Drawing.Size (176,23)
        $l.TabIndex = 0
        $l.Text = ('Menu Group  {0}' -f $cnt)
        $l.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
        $cnt++
      }
      # buttons
      $positions = @{
        'b1' = @{ 'x' = 6; 'y' = 27; };
        'b2' = @{ 'x' = 6; 'y' = 64; };
        'b3' = @{ 'x' = 92; 'y' = 27; };
        'b4' = @{ 'x' = 92; 'y' = 64; };
        'b5' = @{ 'x' = 178; 'y' = 27; };
        'b6' = @{ 'x' = 178; 'y' = 64; };
        'b7' = @{ 'x' = 264; 'y' = 27; };
        'b8' = @{ 'x' = 264; 'y' = 64; };
        'b9' = @{ 'x' = 350; 'y' = 27; };
        'b10' = @{ 'x' = 350; 'y' = 64; };
        'b11' = @{ 'x' = 6; 'y' = 27; };
        'b12' = @{ 'x' = 6; 'y' = 64; };
        'b13' = @{ 'x' = 92; 'y' = 27; };
        'b14' = @{ 'x' = 92; 'y' = 64; };
        'b15' = @{ 'x' = 178; 'y' = 27; };
        'b16' = @{ 'x' = 178; 'y' = 64; };
        'b17' = @{ 'x' = 264; 'y' = 27; };
        'b18' = @{ 'x' = 264; 'y' = 64; };
        'b19' = @{ 'x' = 350; 'y' = 27; };
        'b20' = @{ 'x' = 350; 'y' = 64; };
    
      }
      $cnt = 1
    
      @(
        ([ref]$b1),
        ([ref]$b2),
        ([ref]$b3),
        ([ref]$b4),
        ([ref]$b5),
        ([ref]$b6),
        ([ref]$b7),
        ([ref]$b8),
        ([ref]$b9),
        ([ref]$b10),
        ([ref]$b11),
        ([ref]$b12),
        ([ref]$b13),
        ([ref]$b14),
        ([ref]$b15),
        ([ref]$b16),
        ([ref]$b17),
        ([ref]$b18),
        ([ref]$b19),
        ([ref]$b20)
      ) | ForEach-Object {
        $b = $_.Value
        $b.Name = ('b{0}' -f $cnt)
        $x = $positions[$b.Name].x
        $y = $positions[$b.Name].y
        Write-Debug ('button{0} x = {1}  y = {2}' -f $cnt,$x,$y)
        $b.Location = New-Object System.Drawing.Point ($x,$y)
        $b.Size = New-Object System.Drawing.Size (80,30)
        $b.TabIndex = 1
        $b.Text = ('Button {0}' -f $cnt)
        $b.UseVisualStyleBackColor = $true
        if ($callbacks[$b.Name]) {
          $b.add_click({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              [scriptblock]$s = $callbacks[$sender.Name]
              $local:result = $null
              Invoke-Command $s -ArgumentList $sender,$eventargs
    
    
            })
    
        } else {
          $b.add_click({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              $caller.Data = $sender.Text
              button_click -Sender $sender -eventargs $eventargs
    
            })
    
        }
        $cnt++
    
      }
    
      # Panel1 label and buttons
    
      $p1.Controls.Add($l1)
      $p1.Controls.AddRange(@( $b10,$b9,$b8,$b7,$b6,$b5,$b4,$b3,$b2,$b1))
    
      # Panel2 label and buttons
      $p2.Controls.AddRange(@( $b20,$b19,$b18,$b17,$b16,$b15,$b14,$b13,$b12,$b11))
      $p2.Controls.Add($l2)
    
      #  UserControl1
      $u.AutoScaleDimensions = New-Object System.Drawing.SizeF (6,13)
      $u.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
      $u.BackColor = [System.Drawing.Color]::Gainsboro
    
      $u.Controls.AddRange(@( $p2,$p1))
      $u.Name = 'UserControl1'
      $u.Size = New-Object System.Drawing.Size (948,100)
      $p1.ResumeLayout($false)
      $p2.ResumeLayout($false)
      $u.ResumeLayout($false)

    并显示带有功能区按钮的窗体。

    ribbon

    $caller = New-Object -TypeName 'Win32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    
    PromptRibbon -Title 'Floating Menu Sample Project' -caller $caller
    write-output $caller.Data

    当存在按钮的回调时,它会被运行,否则将调用通用的 button_clisk。完整的脚本源代码可在源 zip 文件中找到。

    自定义调试消息框

    下一个示例显示了 自定义消息框变体,将 C# 代码转换为 PowerShell 语义。

    function return_response
    {
      param(
        [object]$sender,
        [System.EventArgs]$eventargs
      )
    
      [string ]$button_text = ([System.Windows.Forms.Button]$sender[0]).Text
    
      if ($button_text -match '(Yes|No|OK|Cancel|Abort|Retry|Ignore)') {
        $script:Result = $button_text
      }
      $f.Dispose()
    }
    
    function add_buttons {
      param([psobject]$param)
    
      switch ($param) {
        ('None') {
          $button_ok.Width = 80
          $button_ok.Height = 24
          $button_ok.Location = New-Object System.Drawing.Point (391,114)
          $button_ok.Text = 'OK'
          $panel.Controls.Add($button_ok)
          $button_ok.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
    
        }
        ('OK') {
          $button_ok.Width = 80
          $button_ok.Height = 24
          $button_ok.Location = New-Object System.Drawing.Point (391,114)
          $button_ok.Text = 'OK'
          $panel.Controls.Add($button_ok)
          $button_ok.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
        }
        ('YesNo') {
          # add No button
          $button_no.Width = 80
          $button_no.Height = 24
          $button_no.Location = New-Object System.Drawing.Point (391,114)
          $button_no.Text = 'No'
          $panel.Controls.Add($button_no)
          $button_no.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
          # add Yes button
          $button_yes.Width = 80
          $button_yes.Height = 24
          $button_yes.Location = New-Object System.Drawing.Point (($button_no.Location.X - $button_no.Width - 2),114)
          $button_yes.Text = 'Yes'
          $panel.Controls.Add($button_yes)
          $button_yes.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
        }
        ('YesNoCancel') {
          # add Cancel button
          $button_cancel.Width = 80
          $button_cancel.Height = 24
          $button_cancel.Location = New-Object System.Drawing.Point (391,114)
          $button_cancel.Text = 'Cancel'
          $panel.Controls.Add($button_cancel)
          $button_cancel.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
          # add No button
          $button_no.Width = 80
          $button_no.Height = 24
          $button_no.Location = New-Object System.Drawing.Point (($button_cancel.Location.X - $button_cancel.Width - 2),114)
          $button_no.Text = 'No'
          $panel.Controls.Add($button_no)
          $button_no.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
    
          # add Yes button
          $button_yes.Width = 80
          $button_yes.Height = 24
          $button_yes.Location = New-Object System.Drawing.Point (($button_no.Location.X - $button_no.Width - 2),114)
          $button_yes.Text = 'Yes'
          $panel.Controls.Add($button_yes)
          $button_yes_Response.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
        }
        ('RetryCancel') {
          # add Cancel button
          $button_cancel.Width = 80
          $button_cancel.Height = 24
          $button_cancel.Location = New-Object System.Drawing.Point (391,114)
          $button_cancel.Text = 'Cancel'
          $panel.Controls.Add($button_cancel)
          $button_cancel.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
          # add Retry button
          $button_retry.Width = 80
          $button_retry.Height = 24
          $button_retry.Location = New-Object System.Drawing.Point (($button_cancel.Location.X - $button_cancel.Width - 2),114)
          $button_retry.Text = 'Retry'
          $panel.Controls.Add($button_retry)
          $button_retry.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
    
        }
        ('AbortRetryIgnore') {
          # add Ignore button
          $button_ignore.Width = 80
          $button_ignore.Height = 24
          $button_ignore.Location = New-Object System.Drawing.Point (391,114)
          $button_ignore.Text = 'Ignore'
          $panel.Controls.Add($button_ignore)
          $button_ignore.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
          # add Retry button
          $button_retry.Width = 80
          $button_retry.Height = 24
          $button_retry.Location = New-Object System.Drawing.Point (($button_ignore.Location.X - $button_ignore.Width - 2),114)
          $button_retry.Text = 'Retry'
          $panel.Controls.Add($button_retry)
          $button_retry.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
          #add Abort button
          $button_abort.Width = 80
          $button_abort.Height = 24
          $button_abort.Location = New-Object System.Drawing.Point (($button_retry.Location.X - $button_retry.Width - 2),114)
          $button_abort.Text = 'Abort'
          $panel.Controls.Add($button_abort)
          $button_abort.add_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              return_response ($sender,$eventargs)
            })
        }
        default {}
      }
    }
    
    function add_icon_bitmap {
      param([psobject]$param)
    
      switch ($param)
      {
        ('Error') {
          $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Error).ToBitmap()
        }
        ('Information') {
          $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Information).ToBitmap()
        }
        ('Question') {
          $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Question).ToBitmap()
        }
        ('Warning') {
          $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Warning).ToBitmap()
        }
        default {
          $icon_bitmap.Image = ([System.Drawing.SystemIcons]::Information).ToBitmap()
        }
      }
    }
    
    function click_handler
    {
    
      param(
        [object]$sender,
        [System.EventArgs]$eventArgs
      )
      if ($button_details.Tag.ToString() -match 'collapse')
      {
        $f.Height = $f.Height + $txtDescription.Height + 6
        $button_details.Tag = 'expand'
        $button_details.Text = 'Hide Details'
        $txtDescription.WordWrap = true
        # txtDescription.Focus();
        # txtDescription.SelectionLength = 0;
      }
      elseif ($button_details.Tag.ToString() -match 'expand')
      {
        $f.Height = $f.Height - $txtDescription.Height - 6
        $button_details.Tag = 'collapse'
        $button_details.Text = 'Show Details'
      }
    }
    
    function set_message_text
    {
      param(
        [string]$messageText,
        [string]$Title,
        [string]$Description
      )
      $label_message.Text = $messageText
      if (($Description -ne $null) -and ($Description -ne ''))
      {
        $txtDescription.Text = $Description
      }
      else
      {
        $button_details.Visible = $false
      }
      if (($Title -ne $null) -and ($Title -ne ''))
      {
        $f.Text = $Title
      }
      else
      {
        $f.Text = 'Your Message Box'
      }
    }
    
    function Show1
    {
      param(
        [string]$messageText
      )
    
      $f = New-Object System.Windows.Forms.Form
      $button_details = New-Object System.Windows.Forms.Button
      $button_ok = New-Object System.Windows.Forms.Button
      $button_yes = New-Object System.Windows.Forms.Button
      $button_no = New-Object System.Windows.Forms.Button
      $button_cancel = New-Object System.Windows.Forms.Button
      $button_abort = New-Object System.Windows.Forms.Button
      $button_retry = New-Object System.Windows.Forms.Button
      $button_ignore = New-Object System.Windows.Forms.Button
      $txtDescription = New-Object System.Windows.Forms.TextBox
      $icon_bitmap = New-Object System.Windows.Forms.PictureBox
      $panel = New-Object System.Windows.Forms.Panel
      $label_message = New-Object System.Windows.Forms.Label
      set_message_text $messageText '' $null
      add_icon_bitmap -param 'Information'
      add_buttons -param 'OK'
      DrawBox
      [void]$f.ShowDialog()
      Write-Host ('$script:Result = ' + $script:Result)
      $script:Result
    }
    
    function Show2
    {
      param(
        [string]$messageText,
        [string]$messageTitle,
        [string]$description
      )
      $f = New-Object System.Windows.Forms.Form
      $button_details = New-Object System.Windows.Forms.Button
      $button_ok = New-Object System.Windows.Forms.Button
      $button_yes = New-Object System.Windows.Forms.Button
      $button_no = New-Object System.Windows.Forms.Button
      $button_cancel = New-Object System.Windows.Forms.Button
      $button_abort = New-Object System.Windows.Forms.Button
      $button_retry = New-Object System.Windows.Forms.Button
      $button_ignore = New-Object System.Windows.Forms.Button
      $txtDescription = New-Object System.Windows.Forms.TextBox
      $icon_bitmap = New-Object System.Windows.Forms.PictureBox
      $panel = New-Object System.Windows.Forms.Panel
      $label_message = New-Object System.Windows.Forms.Label
      set_message_text $messageText $messageTitle $description
      add_icon_bitmap -param 'Information'
      add_buttons -param 'OK'
      DrawBox
      [void]$f.ShowDialog()
      Write-Host ('$script:Result = ' + $script:Result)
      return $script:Result
    }
    
    function Show3
    {
      param(
        [string]$messageText,
        [string]$messageTitle,
        [string]$description,
        [object]$IcOn,
        [object]$btn
      )
    
      $f = New-Object System.Windows.Forms.Form
      $button_details = New-Object System.Windows.Forms.Button
      $button_ok = New-Object System.Windows.Forms.Button
      $button_yes = New-Object System.Windows.Forms.Button
      $button_no = New-Object System.Windows.Forms.Button
      $button_cancel = New-Object System.Windows.Forms.Button
      $button_abort = New-Object System.Windows.Forms.Button
      $button_retry = New-Object System.Windows.Forms.Button
      $button_ignore = New-Object System.Windows.Forms.Button
      $txtDescription = New-Object System.Windows.Forms.TextBox
      $icon_bitmap = New-Object System.Windows.Forms.PictureBox
      $panel = New-Object System.Windows.Forms.Panel
      $label_message = New-Object System.Windows.Forms.Label
    
      set_message_text $messageText $messageTitle $description
      add_icon_bitmap -param $IcOn
      add_buttons -param $btn
      $script:Result = 'Cancel'
    
      DrawBox
      [void]$f.ShowDialog()
      $f.Dispose()
      Write-Host ('$script:Result = ' + $script:Result)
      return $script:Result
    }
    
    function show_exception
    {
      param([System.Exception]$ex)
    
      $f = New-Object System.Windows.Forms.Form
      $button_details = New-Object System.Windows.Forms.Button
      $button_ok = New-Object System.Windows.Forms.Button
      $button_yes = New-Object System.Windows.Forms.Button
      $button_no = New-Object System.Windows.Forms.Button
      $button_cancel = New-Object System.Windows.Forms.Button
      $button_abort = New-Object System.Windows.Forms.Button
      $button_retry = New-Object System.Windows.Forms.Button
      $button_ignore = New-Object System.Windows.Forms.Button
      $txtDescription = New-Object System.Windows.Forms.TextBox
      $icon_bitmap = New-Object System.Windows.Forms.PictureBox
      $panel = New-Object System.Windows.Forms.Panel
      $label_message = New-Object System.Windows.Forms.Label
      set_message_text -Title 'Exception' -messageText $ex.Message -Description $ex.StackTrace
      add_icon_bitmap -param 'Error'
      add_buttons -param 'YesNo'
      DrawBox
      [void]$f.ShowDialog()
      Write-Host ('$script:Result = ' + $script:Result)
      return $script:Result
    }
    
    function DrawBox
    {
      $f.Controls.Add($panel)
      $panel.Dock = [System.Windows.Forms.DockStyle]::Fill
      # draw picturebox
      $icon_bitmap.Height = 36
      $icon_bitmap.Width = 40
      $icon_bitmap.Location = New-Object System.Drawing.Point (10,11)
      $panel.Controls.Add($icon_bitmap)
      # add textbox
      $txtDescription.Multiline = $true
      $txtDescription.Height = 183
      $txtDescription.Width = 464
      $txtDescription.Location = New-Object System.Drawing.Point (6,143)
      $txtDescription.BorderStyle = [System.Windows.Forms.BorderStyle]::Fixed3D
      $txtDescription.ScrollBars = [System.Windows.Forms.ScrollBars]::Both
      $txtDescription.ReadOnly = $true
      $panel.Controls.Add($txtDescription)
    
      # add detail button
      $button_details.Height = 24
      $button_details.Width = 80
      $button_details.Location = New-Object System.Drawing.Point (6,114)
      $button_details.Tag = 'expand'
      $button_details.Text = 'Show Details'
      $panel.Controls.Add($button_details)
      $button_details.add_click.Invoke({
          param(
            [object]$sender,
            [System.EventArgs]$eventargs
          )
          click_handler ($sender,$eventargs)
        })
    
      $label_message.Location = New-Object System.Drawing.Point (64,22)
      $label_message.AutoSize = $true
      $panel.Controls.Add($label_message)
      $f.Height = 360
      $f.Width = 483
    
      # set form layout
      $f.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
      $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedSingle
      $f.MaximizeBox = $false
      $f.MinimizeBox = $false
      ## frm.FormClosing += new FormClosingEventHandler(frm_FormClosing)
      $f.BackColor = [System.Drawing.SystemColors]::ButtonFace
    
      ## origin http://www.iconarchive.com/search?q=ico+files&page=7
      $f.Icon = New-Object System.Drawing.Icon ([System.IO.Path]::Combine((Get-ScriptDirectory),"Martz90-Circle-Files.ico"))
      if ($button_details.Tag.ToString() -match 'expand')
      {
        $f.Height = $f.Height - $txtDescription.Height - 6
        $button_details.Tag = 'collapse'
        $button_details.Text = 'Show Details'
      }
    }

    结合来自 http://poshcode.org 的纯 PowerShell Assert 函数。

    function assert {
      [CmdletBinding()]
      param(
        [Parameter(Position = 0,ParameterSetName = 'Script',Mandatory = $true)]
        [scriptblock]$Script,
        [Parameter(Position = 0,ParameterSetName = 'Condition',Mandatory = $true)]
        [bool]$Condition,
        [Parameter(Position = 1,Mandatory = $true)]
        [string]$message)
    
      $message = "ASSERT FAILED: $message"
      if ($PSCmdlet.ParameterSetName -eq 'Script') {
        try {
          $ErrorActionPreference = 'STOP'
          $success = & $Script
        } catch {
          $success = $false
          $message = "$message`nEXCEPTION THROWN: $($_.Exception.GetType().FullName)"
        }
      }
      if ($PSCmdlet.ParameterSetName -eq 'Condition') {
        try {
          $ErrorActionPreference = 'STOP'
          $success = $Condition
        } catch {
          $success = $false
          $message = "$message`nEXCEPTION THROWN: $($_.Exception.GetType().FullName)"
        }
      }
    
      if (!$success) {
        $action = Show3 -messageText $message `
           -messageTitle 'Assert failed' `
           -icon $MSGICON.Error `
           -Btn $MSGBUTTON.RetryCancle `
           -Description ("Try:{0}`r`nScript:{1}`r`nLine:{2}`r`nFunction:{3}" -f $Script,(Get-PSCallStack)[1].ScriptName,(Get-PSCallStack)[1].ScriptLineNumber,(Get-PSCallStack)[1].FunctionName)
    
    
        if ($action -ne $MSGRESPONSE.Ignore) {
          throw $message
        }
      }
    }

    稍作修改以显示异常对话框。

    Custom Assert Dialog

    以及调用堆栈信息,并可选地继续执行。

    function Show3
    {
      param(
        [string]$messageText,
        [string]$messageTitle,
        [string]$description,
        [object]$IcOn,
        [object]$btn
      )
    
      $f = New-Object System.Windows.Forms.Form
      $button_details = New-Object System.Windows.Forms.Button
      $button_ok = New-Object System.Windows.Forms.Button
      $button_yes = New-Object System.Windows.Forms.Button
      $button_no = New-Object System.Windows.Forms.Button
      $button_cancel = New-Object System.Windows.Forms.Button
      $button_abort = New-Object System.Windows.Forms.Button
      $button_retry = New-Object System.Windows.Forms.Button
      $button_ignore = New-Object System.Windows.Forms.Button
      $txtDescription = New-Object System.Windows.Forms.TextBox
      $icon_bitmap = New-Object System.Windows.Forms.PictureBox
      $panel = New-Object System.Windows.Forms.Panel
      $label_message = New-Object System.Windows.Forms.Label
    
      set_message_text $messageText $messageTitle $description
      add_icon_bitmap -param $IcOn
      add_buttons -param $btn
      $script:Result = 'Cancel'
    
      DrawBox
      [void]$f.ShowDialog()
      $f.Dispose()
      Write-Host ('$script:Result = ' + $script:Result)
      return $script:Result
    }
    

    您可以使用此片段来处理常规异常。

    Standard Exception Dialog

    或各种按钮组合。完整示例可在源 zip 文件中找到(两个版本:一个保留原始 C# 代码,一个简化版)。

    杂项。密码

    纯文本

    现在,假设任务需要向源代码控制、CI 或其他使用自己的身份验证机制且不接受 NTLM 的远程服务进行身份验证。以下代码有助于提示用户名/密码。它使用了标准的 Windows 窗体实践,即屏蔽密码文本框。

    password

    function PromptPassword(
        [String] $title,
        [String] $user,
            [Object] $caller
        ){
    
            [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
            [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
    
            $f = New-Object System.Windows.Forms.Form
            $f.MaximizeBox = $false;
            $f.MinimizeBox = $false;
            $f.Text = $title
    
            $l1 = New-Object System.Windows.Forms.Label
            $l1.Location = New-Object System.Drawing.Size(10,20)
            $l1.Size = New-Object System.Drawing.Size(100,20)
            $l1.Text = 'Username'
            $f.Controls.Add($l1)
    
            $f.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 10, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);
    
            $t1 = new-object System.Windows.Forms.TextBox
            $t1.Location = new-object System.Drawing.Point(120, 20)
            $t1.Size = new-object System.Drawing.Size(140, 20)
            $t1.Text = $user;
            $t1.Name = 'txtUser';
            $f.Controls.Add($t1);
    
            $l2 = New-Object System.Windows.Forms.Label
            $l2.Location = New-Object System.Drawing.Size(10,50)
            $l2.Size = New-Object System.Drawing.Size(100,20)
            $l2.Text = 'Password'
            $f.Controls.Add($l2)
    
            $t2 = new-object System.Windows.Forms.TextBox
            $t2.Location = new-object System.Drawing.Point(120, 50)
            $t2.Size = new-object System.Drawing.Size(140, 20)
            $t2.Text = ''
            $t2.Name = 'txtPassword'
            $t2.PasswordChar = '*'
            $f.Controls.Add($t2)
    
            $btnOK = new-object System.Windows.Forms.Button
            $x2 = 20
            $y1 = ($t1.Location.Y + $t1.Size.Height + + $btnOK.Size.Height + 20)
            $btnOK.Location = new-object System.Drawing.Point($x2 , $y1 )
            $btnOK.Text = "OK";
            $btnOK.Name = "btnOK";
            $f.Controls.Add($btnOK);
    
            $btnCancel = new-object System.Windows.Forms.Button
            $x1 = (($f.Size.Width -  $btnCancel.Size.Width) - 20 )
    
            $btnCancel.Location = new-object System.Drawing.Point($x1, $y1 );
            $btnCancel.Text = 'Cancel';
            $btnCancel.Name = 'btnCancel';
            $f.Controls.Add($btnCancel);
            $s1 = ($f.Size.Width -  $btnCancel.Size.Width) - 20
            $y2 = ($t1.Location.Y + $t1.Size.Height + $btnOK.Size.Height)
    
            $f.Size = new-object System.Drawing.Size($f.Size.Width,  (($btnCancel.Location.Y +
                                 $btnCancel.Size.Height + 40)))
    
            $btnCancel.Add_Click({$caller.txtPassword = $null ; $caller.txtUser =  $null ;$f.Close()})
            $btnOK.Add_Click({$caller.Data = $RESULT_OK;$caller.txtPassword  = $t2.Text ; $caller.txtUser = $t1.Text;  $f.Close()})
    
    $f.Controls.Add($l)
    $f.Topmost = $true
    
    
    $caller.Data = $RESULT_CANCEL;
    $f.Add_Shown( { $f.Activate() } )
    $f.KeyPreview = $True
    $f.Add_KeyDown({
    
        if ($_.KeyCode -eq 'Escape')  { $caller.Data = $RESULT_CANCEL }
        else                              { return }
        $f.Close()
    })
    
    [Void] $f.ShowDialog([Win32Window ] ($caller) )
    
    $f.Dispose()
    }

    在此脚本中,我们将 Userpassword 存储在单独的字段中。

    $DebugPreference = 'Continue'
    
    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    
    PromptPassword -title 'Enter credentials'  -user 'admin' -caller $caller
    if ($caller.Data -ne $RESULT_CANCEL) {
    write-debug ("Result is : {0} / {1}  " -f  $caller.TxtUser , $caller.TxtPassword )
    }

    Active Directory

    请注意,上面的示例无意收集用户的 NTLM 凭据,例如更改新安装的 Windows 服务以使用所需的用户凭据执行。在这种情况下,请使用 Microsoft Get-Credential cmdlet。

    $DebugPreference = 'Continue'
    
    $target_service_name = 'MsDepSvc'
    $domain = $env:USERDOMAIN
    if ($domain -like 'UAT') {
      $user = '_uatmsdeploy'
    }
    elseif ($domain -like 'PROD') {
      $user = '_msdeploy'
    }
    else {
      $user = $env:USERNAME
    }
    
    $target_account = "${domain}\${user}"
    $credential = Get-Credential -username $target_account -message 'Please authenticate'
    
    if ($credential -ne $null) {
      $target_account  = $credential.Username
      $target_password  = $credential.GetNetworkCredential().Password
      write-Debug $target_password
    } else {
    
    }
    return

    用于凭据验证、管理员权限、修改新安装的服务的代码已从显示中省略。

    password

    会话 Cookie

    另一种可能的登录场景是当用户可以使用其域凭据进行身份验证,但系统在浏览器内部使用会话 cookie。

    您可以创建一个带有 WebBrowser 的对话框,并监视用户何时成功登录,然后收集全局会话 cookie。

    为此,将 wininet.dll p/invoke 代码添加到 $caller 对象并在适当的时候调用。处理浏览器 cookie 的解释可在各种来源中找到,例如此处

    Add-Type -TypeDefinition @"
    // ... c sharp code
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll', 'System.Runtime.InteropServices.dll', 'System.Net.dll'

    使用代码

    using System;
    using System.Text;
    using System.Net;
    using System.Windows.Forms;
    
    using System.Runtime.InteropServices;
    
    public class Win32Window : IWin32Window
    {
        private IntPtr _hWnd;
        private string _cookies;
        private string _url;
    
        public string Cookies
        {
            get { return _cookies; }
            set { _cookies = value; }
        }
    
        public string Url
        {
            get { return _url; }
            set { _url = value; }
        }
    
        public Win32Window(IntPtr handle)
        {
            _hWnd = handle;
        }
    
        public IntPtr Handle
        {
            get { return _hWnd; }
        }
    
        [DllImport("wininet.dll", SetLastError = true)]
        public static extern bool InternetGetCookieEx(
            string url,
            string cookieName,
            StringBuilder cookieData,
            ref int size,
            Int32 dwFlags,
            IntPtr lpReserved);
    
        private const int INTERNET_COOKIE_HTTPONLY = 0x00002000;
        private const int INTERNET_OPTION_END_BROWSER_SESSION = 42;
    
    public string GetGlobalCookies(string uri)
        {
            int datasize = 1024;
            StringBuilder cookieData = new StringBuilder((int)datasize);
            if (InternetGetCookieEx(uri, null, cookieData, ref datasize, INTERNET_COOKIE_HTTPONLY, IntPtr.Zero)
                && cookieData.Length > 0)
            {
                return cookieData.ToString().Replace(';', ',');
            }
            else
            {
                return null;
            }
        }
    
    }

    没有什么可以阻止一个人使用 Add-Type 存储任意有效的 C# 代码。

    password

    并在 $browser 对象中处理 Navigated 事件。

    function promptForContinueWithCookies(
        [String] $login_url = $null,
        [Object] $caller= $null
        )
    {
    
    $f = New-Object System.Windows.Forms.Form
    $f.Text = $title
    
    $timer1 = new-object System.Timers.Timer
    $label1 = new-object System.Windows.Forms.Label
    
    $f.SuspendLayout()
    $components = new-object System.ComponentModel.Container
    
    
            $browser = new-object System.Windows.Forms.WebBrowser
            $f.SuspendLayout();
    
            # webBrowser1
            $browser.Dock = [System.Windows.Forms.DockStyle]::Fill
            $browser.Location = new-object System.Drawing.Point(0, 0)
            $browser.Name = "webBrowser1"
            $browser.Size = new-object System.Drawing.Size(600, 600)
            $browser.TabIndex = 0
            # Form1
            $f.AutoScaleDimensions = new-object System.Drawing.SizeF(6, 13)
            $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
            $f.ClientSize = new-object System.Drawing.Size(600, 600)
            $f.Controls.Add($browser)
            $f.Text = "Login to octopus"
            $f.ResumeLayout($false)
    
    $f.Add_Load({
      param ([Object] $sender, [System.EventArgs] $eventArgs )
    $browser.Navigate($login_url);
    })
    
    $browser.Add_Navigated(
    {
    
      param ([Object] $sender, [System.Windows.Forms.WebBrowserNavigatedEventArgs] $eventArgs )
            # wait for the user to successfully log in
            # then capture the global cookies and sent to $caller
            $url = $browser.Url.ToString()
            if ($caller -ne $null -and $url -ne $null -and $url -match $caller.Url ) {
                $caller.Cookies = $caller.GetGlobalCookies($url)
            }
        }
     )
    
    $f.ResumeLayout($false)
    $f.Topmost = $True
    
    $f.Add_Shown( { $f.Activate() } )
    
    [void] $f.ShowDialog([Win32Window ] ($caller) )
    
    }

    password

    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    $service_host = 'https://:8088'
    $login_route = 'app#/users/sign-in'
    $login_url = ('{0}/{1}' -f $service_host , $login_route)
    
    $caller.Url =  'app#/environments'
    
    promptForContinueWithCookies $login_url $caller
    
    write-host ("{0}->{1}" -f , $caller.Url, $caller.Cookies)

    Cookie 将如下所示:

    OctopusIdentificationToken = 6pivzR9B%2fEOyJwbBkA2XfYe1BW4BNuXUqCtpW7VX943Em%2fkBZataiWxOVRDnsiBz

    通用对话框

    通用对话框是一个很好的候选对象,可以成为 PowerShell 模块(进行中)。

    @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
    
    function TextInputBox {
      param(
        $prompt_message = 'Enter the Value',
        $caption = 'Inputbox Test'
      )
      $script:result = @{ 'text' = ''; 'status' = $null; }
      $form = New-Object System.Windows.Forms.Form
      $label_prompt = New-Object System.Windows.Forms.Label
      $button_ok = New-Object System.Windows.Forms.Button
      $button_cancel = New-Object System.Windows.Forms.Button
      $text_input = New-Object System.Windows.Forms.TextBox
      $form.SuspendLayout()
      $label_prompt.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
      $label_prompt.BackColor = [System.Drawing.SystemColors]::Control
      $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
      $label_prompt.Location = New-Object System.Drawing.Point (12,9)
      $label_prompt.Name = 'lblPrompt'
      $label_prompt.Size = New-Object System.Drawing.Size (302,82)
      $label_prompt.TabIndex = 3
      $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $button_ok.DialogResult = [System.Windows.Forms.DialogResult]::OK
      $button_ok.FlatStyle = [System.Windows.Forms.FlatStyle]::Standard
      $button_ok.Location = New-Object System.Drawing.Point (326,8)
      $button_ok.Name = 'button_ok'
      $button_ok.Size = New-Object System.Drawing.Size (64,24)
      $button_ok.TabIndex = 1
      $button_ok.Text = '&OK'
      $button_ok.Add_Click({
          param([object]$sender,[System.EventArgs]$e)
          $script:result.status = [System.Windows.Forms.DialogResult]::OK
          $script:result.Text = $text_input.Text
          $form.Dispose()
        })
      $button_ok.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $button_cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
      $button_cancel.FlatStyle = [System.Windows.Forms.FlatStyle]::Standard
      $button_cancel.Location = New-Object System.Drawing.Point (326,40)
      $button_cancel.Name = 'button_cancel'
      $button_cancel.Size = New-Object System.Drawing.Size (64,24)
      $button_cancel.TabIndex = 2
      $button_cancel.Text = '&Cancel'
      $button_cancel.Add_Click({
          param([object]$sender,[System.EventArgs]$e)
          $script:result.status = [System.Windows.Forms.DialogResult]::Cancel
          $text_input.Text = ''
          $script:result.Text = ''
          $form.Dispose()
        })
    
      $button_cancel.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $text_input.Location = New-Object System.Drawing.Point (8,100)
      $text_input.Name = 'text_input'
      $text_input.Size = New-Object System.Drawing.Size (379,20)
      $text_input.TabIndex = 0
      $text_input.Text = ''
      $text_input.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
      $form.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
      $form.ClientSize = New-Object System.Drawing.Size (398,128)
      $form.Controls.AddRange(@($text_input,$button_cancel,$button_ok,$label_prompt))
      $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
      $form.MaximizeBox = $false
      $form.MinimizeBox = $false
      $form.Name = 'InputBoxDialog'
      $form.ResumeLayout($false)
      $form.AcceptButton = $button_ok
      $form.ShowInTaskbar = $false
    
      $response = [System.Windows.Forms.DialogResult]::Ignore
      $result = ''
      $text_input.Text = ''
      $label_prompt.Text = $prompt_message
      $form.Text = $caption
      $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
    
      $text_input.SelectionStart = 0;
      $text_input.SelectionLength = $text_input.Text.Length
      $text_input.Focus()
    
      $form.Name = 'Form1'
      $form.ResumeLayout($false)
    
      $form.Topmost = $Trues
    
      $form.Add_Shown({ $form.Activate() })
    
      [void]$form.ShowDialog()
    
      $form.Dispose()
      $form = $null
      return $script:result
    }
    

    combo

    function ComboInputBox {
    
      param(
        [string]$prompt_message = 'Select or Enter the Country',
        [string[]]$items = @(),
        [string]$caption = 'combo test'
      )
    
    function PopulateCombo ()
    {
      param([string[]]$comboBoxItems)
      for ($i = 0; $i -lt $comboBoxItems.Length; $i++)
      {
        $str = $comboBoxItems[$i]
        if ($str -ne $null)
        {
          [void]$combobox.Items.Add($str)
        }
      }
    }
    
      $script:result = @{ 'text' = ''; 'status' = $null; }
      $script:result.status = [System.Windows.Forms.DialogResult]::None;
    
      $form = New-Object System.Windows.Forms.Form
      $label_prompt = New-Object System.Windows.Forms.Label
      $button_ok = New-Object System.Windows.Forms.Button
      $button_cancel = New-Object System.Windows.Forms.Button
      $combobox = New-Object System.Windows.Forms.ComboBox
      $form.SuspendLayout()
      $label_prompt.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
      $label_prompt.BackColor = [System.Drawing.SystemColors]::Control
      $label_prompt.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',8.25,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
      $label_prompt.Location = New-Object System.Drawing.Point (12,9)
      $label_prompt.Name = 'lblPrompt'
      $label_prompt.Size = New-Object System.Drawing.Size (302,82)
      $label_prompt.TabIndex = 3
      $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $button_ok.DialogResult = [System.Windows.Forms.DialogResult]::OK
      $button_ok.FlatStyle = [System.Windows.Forms.FlatStyle]::Standard
      $button_ok.Location = New-Object System.Drawing.Point (326,8)
      $button_ok.Name = 'btnOK'
      $button_ok.Size = New-Object System.Drawing.Size (64,24)
      $button_ok.TabIndex = 1
      $button_ok.Text = '&OK'
      $button_ok.Add_Click({
          param([object]$sender,[System.EventArgs]$e)
          $script:result.status = [System.Windows.Forms.DialogResult]::OK
          $script:result.Text = $combobox.Text
          $form.Dispose()
    
        })
      $button_ok.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $button_cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
      $button_cancel.FlatStyle = [System.Windows.Forms.FlatStyle]::Standard
      $button_cancel.Location = New-Object System.Drawing.Point (326,40)
      $button_cancel.Name = 'btnCancel'
      $button_cancel.Size = New-Object System.Drawing.Size (64,24)
      $button_cancel.TabIndex = 2
      $button_cancel.Text = '&Cancel'
      $button_cancel.Add_Click({
          param([object]$sender,[System.EventArgs]$e)
          $script:result.status = [System.Windows.Forms.DialogResult]::Cancel
          $script:result.Text = ''
          $form.Dispose()
    
        })
      $button_cancel.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $combobox.Location = New-Object System.Drawing.Point (8,100)
      $combobox.Name = 'CmBxComboBox'
      $combobox.Size = New-Object System.Drawing.Size (379,20)
      $combobox.TabIndex = 0
      $combobox.Text = ''
      $combobox.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
      $combobox.Add_TextChanged({
          param([object]$sender,[System.EventArgs]$e)
    
        })
    
      $combobox.Add_KeyPress({
          param(
            [object]$sender,[System.Windows.Forms.KeyPressEventArgs]$e
          )
    
        })
      $combobox.Add_TextChanged({
          param(
            [object]$sender,[System.EventArgs]$e
          )
    
        })
    
    
      $form.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
      $form.ClientSize = New-Object System.Drawing.Size (398,128)
      $form.Controls.AddRange(@($combobox,$button_cancel,$button_ok,$label_prompt))
      $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
      $form.MaximizeBox = $false
      $form.MinimizeBox = $false
      $form.Name = 'ComboBoxDialog'
      $form.ResumeLayout($false)
      $form.AcceptButton = $button_ok
      $script:result.status = [System.Windows.Forms.DialogResult]::Ignore
      $script:result.status = ''
      PopulateCombo -comboBoxItems $items
      $label_prompt.Text = $prompt_message
      $form.Text = $caption
      $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
      $combobox.SelectionStart = 0
      $combobox.SelectionLength = $combobox.Text.Length
      $combobox.Focus()
      $form.Name = 'Form1'
      $form.ResumeLayout($false)
    
      $form.Topmost = $True
    
      $form.Add_Shown({ $form.Activate() })
    
      [void]$form.ShowDialog($caller)
    
      $form.Dispose()
      $form = $null
      return $script:result
    
    }
    
    

    password2

    function ChangePasswordDialogBox {
    
      param(
        [string]$prompt_message = 'Change the password',
        [string]$caption = 'Default Caption',
        [string]$old_password = 'password'
    
      )
      $script:result = @{ 'text' = ''; 'status' = $null; }
    
      $form = New-Object System.Windows.Forms.Form
      $label_old_password = New-Object System.Windows.Forms.Label
      $label_new_password = New-Object System.Windows.Forms.Label
      $label_prompt = New-Object System.Windows.Forms.Label
      $label_confirm_password = New-Object System.Windows.Forms.Label
      $button_ok = New-Object System.Windows.Forms.Button
      $button_cancel = New-Object System.Windows.Forms.Button
      $text_old_password = New-Object System.Windows.Forms.TextBox
      $text_new_password = New-Object System.Windows.Forms.TextBox
      $text_confirm_password = New-Object System.Windows.Forms.TextBox
      $form.SuspendLayout()
      $label_old_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $label_old_password.Location = New-Object System.Drawing.Point (16,88)
      $label_old_password.Name = 'lblOldPassword'
      $label_old_password.Size = New-Object System.Drawing.Size (168,24)
      $label_old_password.TabIndex = 1
      $label_old_password.Text = 'Old Password'
      $label_old_password.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
      $label_new_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $label_new_password.Location = New-Object System.Drawing.Point (16,112)
      $label_new_password.Name = 'lblNewPassword'
      $label_new_password.Size = New-Object System.Drawing.Size (168,24)
      $label_new_password.TabIndex = 2
      $label_new_password.Text = 'New Password'
      $label_new_password.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
      $label_confirm_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $label_confirm_password.Location = New-Object System.Drawing.Point (16,136)
      $label_confirm_password.Name = 'lblConfirmPassword'
      $label_confirm_password.Size = New-Object System.Drawing.Size (168,24)
      $label_confirm_password.TabIndex = 3
      $label_confirm_password.Text = 'Confirm New Password';
      $label_confirm_password.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
      $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
      $label_prompt.Location = New-Object System.Drawing.Point (16,8)
      $label_prompt.Name = 'lblPrompt'
      $label_prompt.Size = New-Object System.Drawing.Size (280,72)
      $label_prompt.TabIndex = 9
      $label_prompt.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
      $label_prompt.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $text_old_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
      $text_old_password.Location = New-Object System.Drawing.Point (192,88)
      $text_old_password.Name = 'txtbxOldPassword'
      $text_old_password.Size = New-Object System.Drawing.Size (184,21);
      $text_old_password.TabIndex = 4
      $text_old_password.Text = ''
      $text_old_password.PasswordChar = '*'
      $text_new_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0);
      $text_new_password.Location = New-Object System.Drawing.Point (192,112)
      $text_new_password.Name = 'txtbxNewPassword'
      $text_new_password.Size = New-Object System.Drawing.Size (184,21)
      $text_new_password.TabIndex = 5
      $text_new_password.Text = ''
      $text_new_password.PasswordChar = '*'
      $text_confirm_password.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
      $text_confirm_password.Location = New-Object System.Drawing.Point (192,136)
      $text_confirm_password.Name = 'txtbxConfirmPassword'
      $text_confirm_password.Size = New-Object System.Drawing.Size (184,21)
      $text_confirm_password.TabIndex = 6
      $text_confirm_password.Text = ''
      $text_confirm_password.PasswordChar = '*'
      $button_ok.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $button_ok.Location = New-Object System.Drawing.Point (312,16)
      $button_ok.Name = 'button_ok'
      $button_ok.Size = New-Object System.Drawing.Size (64,24)
      $button_ok.TabIndex = 7
      $button_ok.Text = 'OK'
      $button_ok.Add_Click({
          param([object]$sender,[System.EventArgs]$e)
          if ($text_old_password.Text.Trim() -ne $old_password) {
            # MessageBox.Show(ChangePasswordDialogBox.frmInputDialog, 'Incorrect Old Password', 'LinkSys', MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            $text_old_password.SelectionStart = 0
            $text_old_password.SelectionLength = $text_old_password.Text.Length
            $text_old_password.Focus()
          } else {
    
            if ($text_new_password.Text.Trim() -ne $text_confirm_password.Text.Trim()) {
    
              $text_confirm_password.SelectionStart = 0
              $text_confirm_passwordSelectionLength = $text_confirm_password.Text.Length
              $text_confirm_password.Focus()
    
            } else {
    
              $script:result.status = [System.Windows.Forms.DialogResult]::OK
              $script:result.Text = $text_new_password.Text
              $form.Dispose()
            } }
    
    
        })
      $button_cancel.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
      $button_cancel.Location = New-Object System.Drawing.Point (312,48)
      $button_cancel.Name = 'btnCancel'
      $button_cancel.Size = New-Object System.Drawing.Size (64,24)
      $button_cancel.TabIndex = 8
      $button_cancel.Text = 'Cancel'
      $button_cancel.Add_Click({
          param([object]$sender,[System.EventArgs]$e)
          $script:result.status = [System.Windows.Forms.DialogResult]::Cancel
          $text_input.Text = ''
          $script:result.Text = ''
          $form.Dispose()
    
        }
      )
      $form.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
      $form.ClientSize = New-Object System.Drawing.Size (400,182)
      $form.Controls.AddRange(@($text_old_password,
    $text_new_password,
    $text_confirm_password,
    $button_cancel,
    $button_ok,
    $label_prompt,
    $label_old_password,
    $label_new_password,
    $label_confirm_password))
      $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
      $form.MaximizeBox = $false
      $form.MinimizeBox = $false
      $form.Name = 'InputBoxDialog'
      $form.ResumeLayout($false)
      $form.AcceptButton = $button_ok
      $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
      $form.ShowInTaskbar = $false
    
      $script:result.status = [System.Windows.Forms.DialogResult]::Ignore
    
      $label_prompt.Text = $prompt_message
      $label_old_password.Text = 'Old Password'
      $label_new_password.Text = 'New Password'
      $label_confirm_password.Text = 'Confirm New Password'
      $text_old_password.Text = $old_password # ''
      $text_new_password.Text = ''
      $text_confirm_password.Text = ''
      $form.Text = $caption
      # Rectangle workingArea = Screen.PrimaryScreen.WorkingArea;
    
      $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
      $text_old_password.Focus()
    
      $form.Name = 'Form1'
      $form.ResumeLayout($false)
    
      $form.Topmost = $Trues
    
      $form.Add_Shown({ $form.Activate() })
    
      [void]$form.ShowDialog()
    
      $form.Dispose()
      $form = $null
      return $script:result
    }
    
    

    chooser

    @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
    $shared_assemblies = @(
      'nunit.framework.dll'
    )
    $shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'
    if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
      $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
    }
    pushd $shared_assemblies_path
    $shared_assemblies | ForEach-Object {
      if ($host.Version.Major -gt 2) {
        Unblock-File -Path $_;
      }
      Write-Debug $_
      Add-Type -Path $_
    }
    popd
    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    $f = New-Object -TypeName 'System.Windows.Forms.Form'
    $f.Text = $title
    $f.SuspendLayout()
    
    $f.AutoScaleDimensions = New-Object System.Drawing.SizeF (6.0,13.0)
    $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
    $f.ClientSize = New-Object System.Drawing.Size (210,105)
    $button_combobox_test = New-Object System.Windows.Forms.Button
    $button_combobox_test.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
    $button_combobox_test.Location = New-Object System.Drawing.Point (10,10)
    $button_combobox_test.Size = New-Object System.Drawing.Size (135,23)
    $button_combobox_test.Text = 'Combobox Test'
    $button_combobox_test.Add_Click({
        $countries = @(
          "India",
          "USA",
          "UK",
          "Russia",
          "Bulgaria",
          "Singapore",
          "Malayasia",
          "Japan",
          "Thailand"
        )
        $prompt_message = 'Select or Enter the Country'
        $caption = 'Combobox Test' 
        $o = ComboInputBox -items $countries -caption  $caption -prompt_message  $prompt_message 
        if ($o.status -match 'OK') {
          $caller.Data = $o.Text
        $f.Close()
        }
      })
    $f.Controls.Add($button_combobox_test)
    $button_change_password_test = New-Object System.Windows.Forms.Button
    $button_change_password_test.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
    $button_change_password_test.Location = New-Object System.Drawing.Point (10,40)
    $button_change_password_test.Size = New-Object System.Drawing.Size (135,23)
    $button_change_password_test.Text = 'Change Password Test'
    $button_change_password_test.Add_Click({
        $prompt_message = 'Change the Password'
        $caption = 'Change Password Test'
        $old_password = '123'
        $o = ChangePasswordDialogBox -prompt_message $prompt_message -caption $caption -old_password $old_password
        if ($o.status -match 'OK') {
          $caller.Data = $o.Text
        $f.Close()
        }
    
      })
    $f.Controls.Add($button_change_password_test)
    $button_inputbox_test = New-Object System.Windows.Forms.Button
    $button_inputbox_test.Font = New-Object System.Drawing.Font ('Arial',10,[System.Drawing.FontStyle]::Bold,[System.Drawing.GraphicsUnit]::Point,0)
    $button_inputbox_test.Location = New-Object System.Drawing.Point (10,70)
    $button_inputbox_test.Size = New-Object System.Drawing.Size (135,23)
    $button_inputbox_test.Text = 'Inputbox test'
    $button_inputbox_test.Add_Click({
        $prompt_message = 'Enter the Value'
        $caption = 'Inputbox test'
        $o = TextInputBox -caption $caption -prompt_message $prompt_message
        if ($o.status -match 'OK') {
          $caller.Data = $o.Text
        $f.Close()
        }
      })
    $f.Controls.Add($button_inputbox_test)
    $f.Name = "Form1"
    $f.Text = 'Standard Input Dialogs'
    $f.ResumeLayout($false)
    $f.Topmost = $Trues
    $f.Add_Shown({ $f.Activate() })
    [void]$f.ShowDialog($caller)
    $f.Dispose()
    Write-Output $caller.Data
    

    完整示例可在源 zip 文件中找到。

    带输入焦点控制的制表对话框

    下一个重要主题是制表对话框。实现此类代码基本上与前面所示的相同,增加了一个额外功能——它会阻止用户离开 textbox ,直到有输入为止。在窗体绘制时,会选择特定的选项卡和输入。

    如果用户尝试在未填写某些文本的情况下切换到其他选项卡或输入,则会在 TextBox 下显示警告消息。

    password

    提供输入后,警告消息会被清除。

    password

    负责此功能的代码突出显示如下:

    function PromptWithTabs(
    [String] $title,
            [Object] $caller
    ){
            [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
            [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
    
            $f = New-Object System.Windows.Forms.Form
            $f.Text = $title
    
            $panel2 = new-object System.Windows.Forms.TabPage
            $textbox1 = new-object System.Windows.Forms.TextBox
            $panel1 = new-object System.Windows.Forms.TabPage
            $button1 = new-object System.Windows.Forms.Button
            $tab_contol1 = new-object System.Windows.Forms.TabControl
            $panel2.SuspendLayout()
            $panel1.SuspendLayout()
            $tab_contol1.SuspendLayout()
            $f.SuspendLayout()
    
            $panel2.Controls.Add($textbox1)
            $panel2.Location = new-object System.Drawing.Point(4, 22)
            $panel2.Name = "tabPage2"
            $panel2.Padding = new-object System.Windows.Forms.Padding(3)
            $panel2.Size = new-object System.Drawing.Size(259, 52)
            $panel2.TabIndex = 1
            $panel2.Text = "Input Tab"
    
            $textbox1.Location = new-object System.Drawing.Point(72, 7)
            $textbox1.Name = "textBoxMessage"
            $textbox1.Size = new-object System.Drawing.Size(100, 20)
            $textbox1.TabIndex = 0
    
            $l1 = New-Object System.Windows.Forms.Label
            $l1.Location = New-Object System.Drawing.Size(72,32)
            $l1.Size = New-Object System.Drawing.Size(100,16)
            $l1.Text = ''
    
            $l1.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 8, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);
            $panel2.Controls.Add($l1)
    
            $textbox1.Add_Leave( {
               param(
                [Object] $sender,
                [System.EventArgs] $eventargs
                )
                if ($sender.Text.length -eq 0) {
                  $l1.Text = 'Input required'
                  # [System.Windows.Forms.MessageBox]::Show('Input required')
                  $tab_contol1.SelectedIndex = 1
                  $sender.Select()
                  $result = $sender.Focus()
                } else {
                  $l1.Text = ''
                }
             })
            $panel1.Controls.Add($button1)
            $panel1.Location = new-object System.Drawing.Point(4, 22)
            $panel1.Name = "tabPage1"
            $panel1.Padding = new-object System.Windows.Forms.Padding(3)
            $panel1.Size = new-object System.Drawing.Size(259, 52)
            $panel1.TabIndex = 0
            $panel1.Text = "Action Tab"
    
            $button1.Location = new-object System.Drawing.Point(74, 7)
            $button1.Name = "buttonShowMessage"
            $button1.Size = new-object System.Drawing.Size(107, 24)
            $button1.TabIndex = 0
            $button1.Text = "Show Message"
            $button1_Click = {
               param(
                [Object] $sender,
                [System.EventArgs] $eventargs
                )
                $caller.Message = $textbox1.Text
                [System.Windows.Forms.MessageBox]::Show($textbox1.Text);
            }
            $button1.Add_Click($button1_Click)
    
            $tab_contol1.Controls.Add($panel1)
            $tab_contol1.Controls.Add($panel2)
            $tab_contol1.Location = new-object System.Drawing.Point(13, 13)
            $tab_contol1.Name = "tabControl1"
            $tab_contol1.SelectedIndex = 1
            $textbox1.Select()
            $textbox1.Enabled = $true
            $tab_contol1.Size = new-object System.Drawing.Size(267, 88)
            $tab_contol1.TabIndex = 0
    
            $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
            $f.ClientSize = new-object System.Drawing.Size(292, 108)
            $f.Controls.Add($tab_contol1)
            $panel2.ResumeLayout($false)
            $panel2.PerformLayout()
            $panel1.ResumeLayout($false)
            $tab_contol1.ResumeLayout($false)
            $f.ResumeLayout($false)
            $f.ActiveControl = $textbox1
    
            $f.Topmost = $true
    
    
            $f.Add_Shown( { $f.Activate() } )
            $f.KeyPreview = $True
    
    
            [Void] $f.ShowDialog([Win32Window ] ($caller) )
    
            $f.Dispose()
    }

    注意:操作顺序在上面的片段中很重要。focus()select() 之间存在细微差别,此处未涵盖。

    password

    单击按钮会启动一个 messagebox ,并将结果存储在 $caller.Message 中。

    进度条

    下一个示例使用基于 Windows Forms 的自定义 ProgressBar Host 来显示,例如,PowerShell 作业在远程主机上执行某些转储任务的状态。

    定义控件类的源代码已导入到脚本中。

    Add-Type -TypeDefinition @"
    
    // "
    
    namespace ProgressBarHost
    {
        public class Progress : System.Windows.Forms.UserControl
        {
        // code
        }
    }
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll', 'System.Drawing.dll', 'System.Data.dll', 'System.ComponentModel.dll'

    在此示例中,PerformStep 方法将不经修改地使用,但它很可能以特定于领域的方式进行自定义。

    PowerShell 脚本执行窗体设计器通常会做的事情。

    $so = [hashtable]::Synchronized(@{
        'Progress'  = [ProgressBarHost.Progress] $null ;
        })
    
    $rs =[runspacefactory]::CreateRunspace()
    $rs.ApartmentState = 'STA'
    $rs.ThreadOptions = 'ReuseThread'
    $rs.Open()
    $rs.SessionStateProxy.SetVariable('so', $so)
    
    
    $run_script = [PowerShell]::Create().AddScript({
    
    function Progressbar(
        [String] $title,
        [String] $message
        ){
    
    [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
    [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
    
    
    $f = New-Object System.Windows.Forms.Form
    $f.Text = $title
    
    $f.Size = New-Object System.Drawing.Size(650,120)
    $f.StartPosition = 'CenterScreen'
    
    $p = new-object ProgressBarHost.Progress
    $p.Location = new-object System.Drawing.Point(12, 8)
    $p.Name = 'status'
    $p.Size = new-object System.Drawing.Size(272, 88)
    $p.TabIndex = 0
    $so.Progress = $p
    
    $b = New-Object System.Windows.Forms.Button
    $b.Location = New-Object System.Drawing.Size(140, 152)
    $b.Size = New-Object System.Drawing.Size(92, 24)
    $b.Text = 'forward'
    $b.Add_Click({ $p.PerformStep()
                   if ($p.Maximum -eq $p.Value) {
                       $b.Enabled = false;
                  }
             })
    
    $f.Controls.Add($b)
    $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 14)
    $f.ClientSize = new-object System.Drawing.Size(292, 194)
    $f.Controls.Add($p )
    $f.Topmost = $True
    $f.Add_Shown( { $f.Activate() } )
    
    [Void] $f.ShowDialog( )
    
    $f.Dispose()
    }
    
    Progressbar -title $title -message $message
    
    })
    
    # -- main program --
    clear-host
    $run_script.Runspace = $rs
    
    $handle = $run_script.BeginInvoke()
    
    start-sleep 3
    $max_cnt  = 10
    $cnt = 0
    while ($cnt -lt $max_cnt) {
       $cnt ++
        Start-Sleep -Milliseconds 1000
        $so.Progress.PerformStep()
    }

    password

    出于调试目的,窗体上添加了带有相同处理程序的 Forward 按钮。为了使脚本的执行成为可能,窗体是从第二个 PowerShell 运行空间启动的。与 caller 参数相反,使用 Synchronized HashTable 对象进行通信。此技术广泛用于 WPF 控件。

    定时器

    下一个示例使用稍作修改的 Timer PowerShell 来显示正在经过的时间,而主 PowerShell 脚本继续执行一些耗时的任务。

    $handle = $run_script.BeginInvoke()
    foreach ($work_step_cnt in @( 1,2,3,5,6,7)) {
      Write-Output ('Doing lengthy work step {0}' -f $work_step_cnt)
      Start-Sleep -Millisecond 1000
    }
    Write-Output 'All Work done'
    $wait_timer_step = 0
    $wait_timer_max = 2

    任务完成后,如果计时器仍然可见,则停止它。

    while (-not $handle.IsCompleted) {
      Write-Output 'waiting on timer to finish'
      $wait_timer_step++
      Start-Sleep -Milliseconds 1000
      if ($wait_timer_step -ge $wait_timer_max) {
        $so.Progress.Value = $so.Progress.Maximum
        Write-Output 'Stopping timer'
        break
      }
    }
    $run_script.EndInvoke($handle)
    $rs.Close()
    return

    timer

    包含进度条和计时器的窗体完全用 PowerShell 编写。

        function GenerateForm {
          param(
            [int]$timeout_sec
          )
    
          @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
    
          $f = New-Object System.Windows.Forms.Form
          $f.MaximumSize = $f.MinimumSize = New-Object System.Drawing.Size (220,65)
          $so.Form = $f
          $f.Text = 'Timer'
          $f.Name = 'form_main'
          $f.ShowIcon = $False
          $f.StartPosition = 1
          $f.DataBindings.DefaultDataSourceUpdateMode = 0
          $f.ClientSize = New-Object System.Drawing.Size (($f.MinimumSize.Width - 10),($f.MinimumSize.Height - 10))
    
          $components = New-Object System.ComponentModel.Container
          $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
          $f.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
          $f.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
    
          $f.SuspendLayout()
    
          $t = New-Object System.Windows.Forms.Timer
    
          $p = New-Object System.Windows.Forms.ProgressBar
          $p.DataBindings.DefaultDataSourceUpdateMode = 0
          $p.Maximum = $timeout_sec
          $p.Size = New-Object System.Drawing.Size (($f.ClientSize.Width - 10),($f.ClientSize.Height - 20))
          $p.Step = 1
          $p.TabIndex = 0
          $p.Location = New-Object System.Drawing.Point (5,5)
          $p.Style = 1
          $p.Name = 'progressBar1'
          $so.Progress = $p
    
          $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
    
          function start_timer {
    
            $t.Enabled = $true
            $t.Start()
    
          }
    
          $t_OnTick = {
            $p.PerformStep()
            $elapsed = New-TimeSpan -Seconds ($p.Maximum - $p.Value)
            $f.Text = ('{0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds)
    
            if ($p.Value -eq $p.Maximum) {
              $t.Enabled = $false
              $f.Close()
            }
          }
    
          $OnLoadForm_StateCorrection = {
            # Correct the initial state of the form to prevent the .Net maximized form issue - http://poshcode.org/1192
            $f.WindowState = $InitialFormWindowState
            start_timer
          }
    
          $elapsed = New-TimeSpan -Seconds ($p.Maximum - $p.Value)
          $f.Text = ('{0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds)
    
          $f.Controls.Add($p)
    
          $t.Interval = 1000
          $t.add_tick($t_OnTick)
    
          $InitialFormWindowState = $f.WindowState
          $f.add_Load($OnLoadForm_StateCorrection)
          [void]$f.ShowDialog()
    
        }

    任务列表进度

    接下来,通过结合 Progressbar 和 Timer 示例与 Task List Progress 程序集,可以为长运行的多步 PowerShell 脚本实现相同的功能。

    下面提供了脚本源(脚本也可以在源 zip 中找到。解释窗体的机制并启用 Skip forward 按钮仍在进行中)。

    $DebugPreference = 'Continue'
    
    $shared_assemblies = @(
      # https://codeproject.org.cn/Articles/11588/Progress-Task-List-Control
      'ProgressTaskList.dll',
      'nunit.core.dll',
      'nunit.framework.dll'
    )
    
    
    $shared_assmblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'
    
    if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
    
      Write-Debug ('Using environment: {0}' -f $env:SHARED_ASSEMBLIES_PATH)
      $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
    }
    
    pushd $shared_assmblies_path
    
    $shared_assemblies | ForEach-Object {
      $assembly = $_
      Write-Debug $assembly
      if ($host.Version.Major -gt 2) {
        Unblock-File -Path $assembly
      }
      Add-Type -Path $assembly
    }
    popd
    
    
    # http://stackoverflow.com/questions/8343767/how-to-get-the-current-directory-of-the-cmdlet-being-executed
    function Get-ScriptDirectory
    {
      $Invocation = (Get-Variable MyInvocation -Scope 1).Value;
      if ($Invocation.PSScriptRoot)
      {
        $Invocation.PSScriptRoot;
      }
      elseif ($Invocation.MyCommand.Path)
      {
        Split-Path $Invocation.MyCommand.Path
      }
      else
      {
        $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
      }
    }

    在此版本中,将使用 ProgressTaskList.dll 的现有功能,不进行任何修改,并将程序集在 Visual Studio 中构建并放置在 $env:SHARED_ASSEMBLIES_PATH 路径下。

    实际的工作步骤将在主脚本中执行,因此窗体将在单独的 Runspace 中执行。

    $so = [hashtable]::Synchronized(@{
        'Title' = [string]'';
        'Visible' = [bool]$false;
        'ScriptDirectory' = [string]'';
        'Form' = [System.Windows.Forms.Form]$null;
        'DebugMessage' = '';
        'Current' = 0;
        'Previous' = 0;
        'Last' = 0;
        'Tasks' = [System.Management.Automation.PSReference];
        'Progress' = [Ibenza.UI.Winforms.ProgressTaskList]$null;
      })

    在窗体的 timer 回调中,使用 $so.Current$so.Last$so.Previous 来检测何时调用放置在窗体上的 Ibenza.UI.Winforms.ProgressTaskList 对象的 NextTask()

    $so.ScriptDirectory = Get-ScriptDirectory
    $rs = [runspacefactory]::CreateRunspace()
    $rs.ApartmentState = 'STA'
    $rs.ThreadOptions = 'ReuseThread'
    $rs.Open()
    $rs.SessionStateProxy.SetVariable('so',$so)
    
    $run_script = [powershell]::Create().AddScript({

    在窗体中,实例化一个 System.Windows.Forms.Timer 对象来检查主脚本中执行的 Tasks 的状态。还有一个 System.Windows.Forms.Button 用于推送当前任务,其功能未完成,因此其状态被禁用。

        function ProgressbarTasklist {
          param(
            [string]$title,
            [System.Management.Automation.PSReference]$tasks_ref,
            [object]$caller
          )
    
          @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
    
          $f = New-Object -TypeName 'System.Windows.Forms.Form'
          $so.Form = $f
          $f.Text = $title
          $t = New-Object System.Windows.Forms.Timer
          $so.DebugMessage = '"in form"'
          function start_timer {
    
            $t.Enabled = $true
            $t.Start()
    
          }
    
          $t_OnTick = {
            # TODO 
            # $elapsed = New-TimeSpan -Seconds ($p.Maximum - $p.Value)
            # $text = ('{0:00}:{1:00}:{2:00}' -f $elapsed.Hours,$elapsed.Minutes,$elapsed.Seconds)
            if ($so.Current -eq $so.Last) {
              $t.Enabled = $false
              $so.DebugMessage = '"Complete"'
              $f.Close()
            } else {
              $so.DebugMessage = '"in timer"'
              if ($so.Current -gt $so.Previous) {
                $o.NextTask()
                $so.Previous = $so.Current
                $so.DebugMessage = ('Finished "{0}"' -f $so.Previous )
              }
            }
          }
          $t.Interval = 300
          $t.add_tick($t_OnTick)
    
          $f.Size = New-Object System.Drawing.Size (650,150)
          $f.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
          $f.AutoScaleBaseSize = New-Object System.Drawing.Size (5,14)
          $f.ClientSize = New-Object System.Drawing.Size (292,144)
    
    
          $panel = New-Object System.Windows.Forms.Panel
          $panel.BackColor = [System.Drawing.Color]::Silver
          $panel.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
    
          $b = New-Object System.Windows.Forms.Button
          $b.Location = New-Object System.Drawing.Point (210,114)
          $b.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
          $b.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',7,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
    
          $b.Text = 'Skip forward'
          [scriptblock]$progress = {
    
            if (-not $o.Visible) {
              # set the first task to 'in progress'
              $o.Visible = $true
              $so.Current = 1
              $o.Start()
    
            } else {
              # TODO: set the following task to 'skipped'
              $so.Current = $so.Current + 1
              $so.DebugMessage = ('Skipped "{0}"' -f $so.Current )
              $o.NextTask()
            }
          }
    
          $progress_click = $b.add_click
          $progress_click.Invoke({
              param(
                [object]$sender,
                [System.EventArgs]$eventargs
              )
              if ($so.Current -eq $so.Last)
              {
                $b.Enabled = $false
                Start-Sleep -Millisecond 300
                $so.Current = $so.Current + 1
                $so.Visible = $false
              } else {
                Invoke-Command $progress -ArgumentList @()
              }
    
            })
          $b.Enabled = $false
          $o = New-Object -TypeName 'Ibenza.UI.Winforms.ProgressTaskList' -ArgumentList @()
          $o.BackColor = [System.Drawing.Color]::Transparent
          $o.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
          $o.Dock = [System.Windows.Forms.DockStyle]::Fill
          $o.Location = New-Object System.Drawing.Point (0,0)
          $o.Name = "progressTaskList1"
          $o.Size = New-Object System.Drawing.Size (288,159)
          $o.TabIndex = 2
          $so.Progress = $o
          $o.TaskItems.AddRange(@( [string[]]$tasks_ref.Value))
    
          $so.Last = $tasks_ref.Value.Count + 1 # will use 1-based index 
          $o.Visible = $false
          $panel.SuspendLayout()
          $panel.ForeColor = [System.Drawing.Color]::Black
          $panel.Location = New-Object System.Drawing.Point (0,0)
          $panel.Name = 'panel'
          $panel.Size = New-Object System.Drawing.Size (($f.Size.Width),($f.Size.Height))
          $panel.TabIndex = 1
    
          $panel.Controls.Add($o)
          $panel.ResumeLayout($false)
          $panel.PerformLayout()
          $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
    
          $f.Controls.AddRange(@( $b,$panel))
          $f.Topmost = $True
    
          $so.Visible = $true
          $f.Add_Shown({
              $f.WindowState = $InitialFormWindowState
              $f.Activate()
              Invoke-Command $progress -ArgumentList @()
              start_timer
            })
          [void]$f.ShowDialog()
    
          $f.Dispose()
        }
        $tasks_ref = $so.Tasks
        ProgressbarTasklist -tasks_ref $tasks_ref -Title $so.Title
        Write-Output ("Processed:`n{0}" -f ($tasks_ref.Value -join "`n"))
      })

    运行在默认 runspace 中的调用脚本更新 $so.Current,从而在执行适当步骤后向窗体的 timer 发出信号——目前它休眠随机时间,不超过 5 秒。此外,它会将进度消息打印到控制台,尽管良好的同步不是本示例的主要目的。假定实际工作会产生大量额外的屏幕输出,使得很难发现何时完成某个步骤。

    $tasks = @(
      'Verifying cabinet integrity',
      'Checking necessary disk space',
      'Extracting files',
      'Modifying registry',
      'Installing files',
      'Removing temporary files')
    
    
    $task_status = @{}
    
    $tasks | ForEach-Object { $task_status[$_] = $null }
    
    $so.Tasks = ([ref]$tasks)
    $so.Title = 'Task List'
    
    $run_script.Runspace = $rs
    
    $handle = $run_script.BeginInvoke()
    
    function PerformStep {
    
      param(
        [int]$step,
        [switch]$skip
      )
      $task_status[$step] = $true
    
      $so.Current = $step
      # can call Progress class methods across Runspaces 
      # $so.Progress.NextTask() 
    
    }
    
    Start-Sleep -Millisecond 100
    while ($so.Visible) {
      for ($cnt = 0; $cnt -ne $tasks.Count; $cnt++) {
        $step_name = $tasks[$cnt]
        Start-Sleep -Milliseconds (Get-Random -Maximum 5000)
        PerformStep -Step $cnt
        Write-Host ('Completes step [{0}] "{1}"' -f $cnt,$step_name)
      }
      $so.Visible = $false
    }
    Write-Output $so.DebugMessage
    # Close the progress form
    $so.Form.Close()
    
    
    $run_script.EndInvoke($handle)
    $rs.Close()

    一切完成后,窗体关闭,运行空间被销毁。

    Three button prompt

    如果您要修改 Ibenza.UI.Winforms.ProgressTaskList 的源代码,首先将类的设计器生成的代码存储在脚本中作为 Add-TypeTypeDefinition 参数。唯一需要的修改是从 https://www.iconfinder.com 下载合适的 16x16 图标并替换

    this.imageList1.ImageStream = ((System.Windows.Forms.ImageListStreamer)(resources.GetObject("imageList1.ImageStream")))

            private string[] iconPaths = new string[] {
              @"C:\developer\sergueik\powershell_ui_samples\1420429962_216151.ico",        
              @"C:\developer\sergueik\powershell_ui_samples\1420429337_5880.ico",
              @"C:\developer\sergueik\powershell_ui_samples\1420429523_62690.ico",        	
              @"C:\developer\sergueik\powershell_ui_samples\1420429596_9866.ico"        
            } ;
    ...
    foreach (string iconPath in this.iconPaths)
                {
                    this.imageList1.Images.Add(new Icon(iconPath));
                }

    下一步是重构 PowerShell 脚本,暂时移除额外的 runspace 和计时器对象,并将重点放在按钮上。

      $b = New-Object System.Windows.Forms.Button
      $b.Location = New-Object System.Drawing.Point (210,114)
      $b.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',7,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
    
      $b.Text = 'forward'
    
      $b.add_click({
    
          if ($caller.Current -eq $caller.Last)
          {
            $b.Enabled = false
          } else {
    
            if (-not $o.Visible) {
              # set the first task to 'in progress'
              $o.Visible = $true
              $caller.Current = 1
              $o.Start()
    
    
            } else {
              # set the following task to 'in progress'
              $o.NextTask()
              $caller.Current = $caller.Current + 1
            }
          }
        })
      # original assembly
      # $i = New-Object -TypeName 'Ibenza.UI.Winforms.ProgressTaskList' -ArgumentList @()
      $o = New-Object -TypeName 'WIP.ProgressTaskList' -ArgumentList @()

    在上面,引入了 $caller 对象来存储 CurrentLast 索引。

    圆形进度指示器

    Circle Progress

    下一个示例结合了 异步 GUIProgressCircle-进度控件,以生成一个通过 PowerShell 运行空间直接调用窗体元素控制的单个进程圆形进度指示器。

    窗体(不包括 ProgressCircle.ProgressCircle Add-Type)是

    Add-Type -AssemblyName 'System.Windows.Forms'
    Add-Type -AssemblyName 'System.Drawing'
    # VisualStyles are only needed for a very few Windows Forms controls like ProgessBar
    [void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms.VisualStyles')
    
    
    $Form = New-Object System.Windows.Forms.Form
    $l1 = New-Object System.Windows.Forms.Label
    $is= New-Object System.Windows.Forms.FormWindowState
    $Form.Text = 'Demo Form'
    $Form.Name = 'Form'
    $Form.DataBindings.DefaultDataSourceUpdateMode = 0
    $Form.ClientSize = New-Object System.Drawing.Size (216,121)
    # Label 
    $l1.Name = 'progress_label'
    $l1.Location = New-Object System.Drawing.Point (70,34)
    $l1.Size = New-Object System.Drawing.Size (100,23)
    $l1.Text = 'Round:'
    
    
    #  progressCircle1
    $c1 = New-Object -TypeName 'ProgressCircle.ProgressCircle'
    $c1.Location = New-Object System.Drawing.Point (20,20)
    $c1.Name = "progress_circle"
    $c1.PCElapsedTimeColor1 = [System.Drawing.Color]::Chartreuse
    $c1.PCElapsedTimeColor2 = [System.Drawing.Color]::Yellow
    $c1.PCLinearGradientMode = [System.Drawing.Drawing2D.LinearGradientMode]::Vertical
    $c1.PCRemainingTimeColor1 = [System.Drawing.Color]::Navy
    $c1.PCRemainingTimeColor2 = [System.Drawing.Color]::LightBlue
    $c1.PCTotalTime = 25
    $c1.Size = New-Object System.Drawing.Size (47,45)
    $c1.TabIndex = 3
    $progress_complete = $c1.add_PCCompleted
    $progress_complete.Invoke({
        param([object]$sender,[string]$message)
        # [System.Windows.Forms.MessageBox]::Show('Task completed!')
        $l1.Text = ('Task completed!')
      })
    
    
    $Form.Controls.AddRange(@($l1,$c1))
    
    $is= $Form.WindowState
    
    $Form.add_Load({
        $Form.WindowState = $InitialFormWindowState
      })

    调用者构造 System.EventArgs 对象以在 ProgressCircle.ProgressCircle 控件上执行委托,该委托会递增并更新按名称找到的相应 Label。请注意,有几种方法可以做到这一点。

    $rs = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($Host)
    $rs.ApartmentState = 'STA'
    $rs.ThreadOptions = 'ReuseThread'
    $rs.Open()
    
    $rs.SessionStateProxy.SetVariable('Form',$Form)
    $po = [System.Management.Automation.PowerShell]::Create()
    $po.Runspace = $rs
    
    $po.AddScript({
        [System.Windows.Forms.Application]::EnableVisualStyles()
        [System.Windows.Forms.Application]::Run($Form)
      })
    
    $res = $po.BeginInvoke()
    
    if ($PSBoundParameters['pause']) {
      Write-Output 'Pause'
      try {
        [void]$host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
      } catch [exception]{}
    } else {
      Start-Sleep -Millisecond 1000
    }
    
    
    # subclass
    $eventargs = New-Object -TypeName 'System.EventArgs'
    
    Add-Member -InputObject $eventargs -MemberType 'NoteProperty' -Name 'Increment' -Value 0 -Force
    Add-Member -InputObject $eventargs -MemberType 'NoteProperty' -Name 'Total' -Value 0 -Force
    
    $handler = [System.EventHandler]{
      param(
        [object]$sender,
        [System.EventArgs]$e
      )
      $local:increment = $e.Increment
      $local:total = $e.Total
      $sender.Increment($local:increment)
      $sender.Text = $e.MyText
      try {
        $elems = $sender.Parent.Controls.Find('progress_label',$false)
      } catch [exception]{
      }
      if ($elems -ne $null) {
        $elems[0].Text = ('Round: {0}' -f $local:total)
      }
    
    }
    
    1..25 | ForEach-Object {
    
      $eventargs.Total = $_
      $eventargs.Increment = 1
      [void]$c1.BeginInvoke($handler,($c1,([System.EventArgs]$eventargs)))
    
      Start-Sleep -Milliseconds (Get-Random -Maximum 1000)
    
    }
    
    if ($PSBoundParameters['pause']) {
      # block PowerShell Main-Thread to leave it alive until user enter something
      Write-Output 'Pause'
      try {
        [void]$host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
      } catch [exception]{}
    } else {
      Start-Sleep -Millisecond 2000
    }
    
    
    [System.Windows.Forms.Application]::Exit()
    $po.EndInvoke($res)
    $rs.Close()
    $po.Dispose()

    注意:要在 W2K3 上运行脚本,您必须触发另一个调用(更新的脚本可在源 zip 中找到)。

    1..($total_steps ) | ForEach-Object {
    
      $current_step = $_
      $message = $eventargs.Text =( 'Processed {0} / {1}' -f $current_step , $total_steps )
    
      $eventargs.Increment = 1
      [void]$c1.BeginInvoke($handler,($c1,([System.EventArgs]$eventargs)))
      if ($host.Version.Major -eq 2) {
        $c1.Invoke(
    
            [System.Action[int, string]] { 
                param(
                  [int]$increment, 
                  [string]$message
                )
                $sender.Increment($increment) 
                try {
                  $elems = $sender.Parent.Controls.Find('progress_label',$false)
                } catch [exception]{
                }
                if ($elems -ne $null) {
                  $elems[0].Text = $message
                }
    
            },
    
            # Argument for the System.Action delegate scriptblock
    
            @(1, $message)
    
        )
      }
      Start-Sleep -Milliseconds (Get-Random -Maximum 1000)
    }

    多任务进度跟踪的泛化正在进行中。完整的示例代码在源 zip 中提供。

    可以使用 Mac OS X 样式进度圆形,只需对 C# 代码进行最少的修改。

    Add-Type -TypeDefinition @"
    
    // "
    
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Data;
    using System.Text;
    using System.Windows.Forms;
    
    namespace ProgressControl
    {
        public partial class CircularProgressControl : UserControl
        {
        // ... omitted most of the code
            public enum Direction
            {
                CLOCKWISE,
                ANTICLOCKWISE
            }
            public Direction Rotation { get; set; }
            private bool m_clockwise;
            public bool Clockwise 
            {
                get
                {
                    return m_clockwise;
                }
                set
                {
                    m_clockwise = value;
                    if (m_clockwise){ 
                       this.Rotation = Direction.CLOCKWISE;
                    } else { 
                       this.Rotation = Direction.ANTICLOCKWISE;
                     }
                }
            }
    
         // .. the rest of the class definition
    
        }
    }
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll','System.Drawing.dll','System.Data.dll'
    
    

    脚本的 PowerShell 部分是:

    @( 'System.Drawing','System.Windows.Forms') | ForEach-Object { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
    $f = New-Object System.Windows.Forms.Form
    $f.AutoScaleDimensions = New-Object System.Drawing.SizeF (6.0,13.0)
    $f.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
    $f.BackColor = [System.Drawing.Color]::LightGray
    $f.ClientSize = New-Object System.Drawing.Size (170,140)
    
    $button1 = New-Object System.Windows.Forms.Button
    $cbc1 = New-Object ProgressControl.CircularProgressControl
    $cbc2 = New-Object ProgressControl.CircularProgressControl
    $f.SuspendLayout()
    
    $button1.Location = New-Object System.Drawing.Point (70,80)
    $button1.Name = "button1"
    $button1.Size = New-Object System.Drawing.Size (75,23)
    $button1.TabIndex = 0
    $button1.Text = "Start"
    $button1.UseVisualStyleBackColor = true
    $button1.add_click.Invoke({
        param(
          [object]$sender,
          [System.EventArgs]$eventargs
        )
        if ($button1.Text -eq "Start")
        {
          $button1.Text = 'Stop'
          $cbc1.Start()
          $cbc2.Start()
        }
        else
        {
          $button1.Text = 'Start'
          $cbc1.Stop()
          $cbc2.Stop()
        }
      })
    
    
    $cbc1.BackColor = [System.Drawing.Color]::Transparent
    $cbc1.Interval = 60
    $cbc1.Location = New-Object System.Drawing.Point (10,20)
    $cbc1.MinimumSize = New-Object System.Drawing.Size (56,56)
    $cbc1.Name = "circularProgressControl1"
    $cbc1.Clockwise = $true
    $cbc1.Size = New-Object System.Drawing.Size (56,56)
    $cbc1.StartAngle = 270
    $cbc1.TabIndex = 1
    $cbc1.TickColor = [System.Drawing.Color]::DarkBlue
    
    $cbc2.BackColor = [System.Drawing.Color]::Transparent
    $cbc2.Interval = 60
    $cbc2.Location = New-Object System.Drawing.Point (10,80)
    $cbc2.MinimumSize = New-Object System.Drawing.Size (56,56)
    $cbc2.Name = "$cbc2"
    $cbc2.Clockwise = $false
    $cbc2.Size = New-Object System.Drawing.Size (56,56)
    $cbc2.StartAngle = 270
    $cbc2.TabIndex = 2
    $cbc2.TickColor = [System.Drawing.Color]::Yellow
    
    $f.Controls.Add($cbc2)
    $f.Controls.Add($button1)
    $f.Controls.Add($cbc1)
    $f.Name = "Form1"
    $f.Text = 'OS X Progress Control'
    $f.ResumeLayout($false)
    
    [void]$f.ShowDialog()
    
    

    osx circle progress

    文件系统树视图

    下一个示例将 文件系统树视图 自定义为 PowerShell。在 Add-Type -TypeDefinition 中,结合了 FileSystemTreeViewShellIcon 类的实现。

    using System;
    using System.IO;
    using System.Windows.Forms;
    using System.ComponentModel;
    using System.Collections;
    using System.Drawing;
    using System.Runtime.InteropServices;
    
    namespace C2C.FileSystem
    {
        public class FileSystemTreeView : TreeView
        {
    ...  
        }
        public class ShellIcon
        {
    ... 
     }
    
    }

    在 PowerShell 部分,向 C2C.FileSystem.FileSystemTreeView 添加 AfterSelect 处理程序,其中选定的 TreeNode FullPath 被存储并写入文本框。$show_files_checkbox 复选框允许在运行时打开和关闭 LoadFiles

    treeview

    $caller = New-Object -TypeName 'Win32Window' -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    $chooser = New-Object -TypeName 'C2C.FileSystem.FileSystemTreeView' -ArgumentList ($caller)
    
    [void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
    [void][System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
    [void][System.Reflection.Assembly]::LoadWithPartialName('System.Data')
    
    # set up form
    $form = New-Object System.Windows.Forms.Form
    $form.Text = $title
    
    
    $form.Size = New-Object System.Drawing.Size (700,450)
    
    $panel = New-Object System.Windows.Forms.Panel
    
    
    $panel1 = New-Object System.Windows.Forms.Panel
    $btnDirectory = New-Object System.Windows.Forms.Button
    $label1 = New-Object System.Windows.Forms.Label
    $txtDirectory = New-Object System.Windows.Forms.TextBox
    $treePanel = New-Object System.Windows.Forms.Panel
    $panel1.SuspendLayout()
    $form.SuspendLayout()
    
    # 
    # panel1
    # 
    $panel1.Controls.Add($btnDirectory)
    $panel1.Controls.Add($label1)
    $panel1.Controls.Add($txtDirectory)
    $panel1.Dock = [System.Windows.Forms.DockStyle]::Top
    $panel1.Location = New-Object System.Drawing.Point (0,0)
    $panel1.Name = 'panel1'
    $panel1.Size = New-Object System.Drawing.Size (681,57)
    $panel1.TabIndex = 0
    
    $show_files_checkbox = New-Object System.Windows.Forms.CheckBox
    $show_files_checkbox.Location = New-Object System.Drawing.Point (515,27)
    $show_files_checkbox.Size = New-Object System.Drawing.Size (120,20)
    $show_files_checkbox.Text = 'Files'
    
    $panel1.Controls.Add($show_files_checkbox)
    $show_files_checkbox.add_click({ if ($show_files_checkbox.Checked -eq $true) { $chooser.ShowFiles = $true } else { $chooser.ShowFiles = $false } })
    
    
    # 
    # btnDirectory
    # 
    $btnDirectory.Location = New-Object System.Drawing.Point (560,27)
    $btnDirectory.Name = "btnDirectory"
    $btnDirectory.Size = New-Object System.Drawing.Size (60,21)
    $btnDirectory.TabIndex = 2
    $btnDirectory.Text = 'Select'
    $btnDirectory.add_click({ if ($caller.Data -ne $null) { $form.Close() } })
    
    # 
    # label1
    # 
    $label1.Location = New-Object System.Drawing.Point (9,9)
    $label1.Name = 'label1'
    $label1.Size = New-Object System.Drawing.Size (102,18)
    $label1.TabIndex = 1
    $label1.Text = 'Selection:'
    
    # 
    # txtDirectory
    # 
    $txtDirectory.Location = New-Object System.Drawing.Point (9,27)
    $txtDirectory.Name = "txtDirectory"
    $txtDirectory.Size = New-Object System.Drawing.Size (503,20)
    $txtDirectory.TabIndex = 0
    $txtDirectory.Text = ""
    
    # 
    # treePanel
    # 
    $treePanel.Dock = [System.Windows.Forms.DockStyle]::Fill
    $treePanel.Location = New-Object System.Drawing.Point (0,57)
    $treePanel.Name = "treePanel"
    $treePanel.Size = New-Object System.Drawing.Size (621,130)
    $treePanel.TabIndex = 1
    
    $treePanel.Controls.Add($chooser)
    $chooser.ShowFiles = $false
    $chooser.Dock = [System.Windows.Forms.DockStyle]::Fill
    $chooser.Add_AfterSelect({ $txtDirectory.Text = $caller.Data = $chooser.Data })
    $chooser.Load('C:\')
    # Form1
    # 
    $form.AutoScaleBaseSize = New-Object System.Drawing.Size (5,13)
    $form.ClientSize = New-Object System.Drawing.Size (621,427)
    $form.Controls.Add($treePanel)
    $form.Controls.Add($panel1)
    $form.Name = 'Form1'
    $form.Text = 'Demo Chooser'
    $panel1.ResumeLayout($false)
    $form.ResumeLayout($false)
    $form.Add_Shown({ $form.Activate() })
    $form.KeyPreview = $True
    $form.Add_KeyDown({
    
        if ($_.KeyCode -eq 'Escape') { $caller.Data = $null }
        else { return }
        $form.Close()
      })
    
    [void]$form.ShowDialog([win32window ]($caller))
    
    $form.Dispose()
    Write-Output $caller.Data

    完整的脚本源代码可在源 zip 文件中找到。

    嵌入 XAML

    password

    设计 WPF XAML 更简单。

    Add-Type -AssemblyName PresentationFramework
    [xml]$xaml =
    @"
    <?xml version="1.0"?>
    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Row GridSplitter Example">
      <StackPanel Height="Auto">
        <Grid Height="400">
          <Grid.RowDefinitions>
            <RowDefinition Height="50*"/>
            <RowDefinition Height="50*"/>
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
          </Grid.ColumnDefinitions>
          <Button Background="gray" Grid.Column="0"
          Grid.Row="0" x:Name="button00" HorizontalAlignment="Stretch"
          VerticalAlignment="Stretch" Content="Quentin Tarantino"/>
          <Button Background="gray" Grid.Column="0" Grid.Row="1"
          x:Name="button01" HorizontalAlignment="Stretch"
          VerticalAlignment="Stretch" Content="Larry Dimmick"/>
          <Button Background="gray" Grid.Column="1" Grid.Row="0"
          x:Name="button10" HorizontalAlignment="Stretch"
          VerticalAlignment="Stretch" Content="Steve Buscemi"/>
          <Button Background="gray" Grid.Column="1" Grid.Row="1"
          x:Name="button11" HorizontalAlignment="Stretch"
          VerticalAlignment="Stretch" Content="Tim Roth"/>
        </Grid>
      </StackPanel>
    </Window>
    "@

    现在,System.Windows.Window 不接受 IWin32Window 参数。

    $colors = @{
      'Steve Buscemi' = ([System.Windows.Media.Colors]::Pink);
      'Larry Dimmick' = ([System.Windows.Media.Colors]::White);
      'Quentin Tarantino' = ([System.Windows.Media.Colors]::Orange);
      'Tim Roth' = ([System.Windows.Media.Colors]::Brown);
    }
    
    $result = @{ }
    
    $DebugPreference = 'Continue'
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $target=[Windows.Markup.XamlReader]::Load($reader )
    $target.ShowDialog() | out-null
    # $result | format-table

    对于简单的行为,一种将结果传回脚本的方法是通过在脚本中定义并在事件处理程序中可见的 $result 哈希变量。

    foreach ($button in @("button01" , "button00", "button10", "button11")) {
      $control=$target.FindName($button)
      $eventMethod=$control.add_click
      $eventMethod.Invoke({
         param(
           [Object] $sender,
           [System.Windows.RoutedEventArgs ] $eventargs
         )
        $who = $sender.Content.ToString()
        $color = $colors[$who ]
        # $target.Title=("You will be  Mr. {0}" -f  $color)
        $sender.Background = new-Object System.Windows.Media.SolidColorBrush($color)
        $result[ $who  ] = $true
        write-debug $who
      })
    }

    此示例很简单——同一个事件处理程序附加到 XAML 流中的每个可点击元素。发送者的详细信息存储在 $result 中,同时为了提供视觉提示,代码会更改 $sender 的背景。

    ...即时

    另一个例子是,可以使用以下代码片段通过 XAML 动态生成 ComboBox 源列表:

    $items = @(
       'Apple' ,
       'Banana' ,
       'Orange' ,
       'Pineapple' ,
       'Plum'
       )
    $selected = @{ }
    $context = @'
    <window height="60" title="Window1" width="200" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <stackpanel>
        <combobox iseditable="False" margin="5" name="comboBox">
    '@
    $cnt = 1
    $items | foreach-object {  $name = "Item_${cnt}" ; $cnt ++ ; $context +="<comboboxitem content="$_" name="${name}">" }
    $context += @'
            </comboboxitem></combobox>
        </stackpanel>
    </window>
    '@
    
    
    Add-Type -AssemblyName PresentationFramework
    [xml]$xaml = $context
    
    Clear-Host
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $target=[Windows.Markup.XamlReader]::Load($reader)
    $handler = {
          param ([object] $sender,  # System.Windows.Controls.ComboboxItem
                                    # http://msdn.microsoft.com/en-us/library/system.windows.controls.comboboxitem_properties%28v=vs.110%29.aspx
                 [System.Windows.RoutedEventArgs] $eventargs )
          $sender.Background = [ System.Windows.Media.Brushes]::Red
          $target.Title = ( 'Added {0} ' -f $sender.Content )
          $selected[ $sender.Content  ]  = $true
      }

    此代码为项目选择提供了最小但清晰的视觉反馈。

    foreach ($item in ("Item_1", "Item_5", "Item_2","Item_3","Item_4") ){
      $combobox_item_control = $target.FindName( $item )
      $eventargsventMethod2 = $combobox_item_control.add_Selected
      $eventargsventMethod2.Invoke( $handler )
      $combobox_item_control = $null
    }

    产生

    TreeView

    并以 PowerShell 的方式打印选定的结果。

    $target.ShowDialog() | out-null
    write-output 'Selected items:'$items | where-object {$selected.ContainsKey( $_ ) }

    TreeView

    更多

    值得注意的是,您可以在纯 XAML 中设计一个非常丰富的用户界面,同时保持实际选择处理的简单性。

    例如,通过(在很大程度上)重复之前的练习,但在面板上绘制 3 个填充颜色的箭头多边形。

    Add-Type -AssemblyName PresentationFramework
    [xml]$xaml = @"
    // .... code below
    "@
    
    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="100" Width="200" Title="Window1">
      <Canvas Height="100" Width="200" Name="Canvas1">
        <!-- Draws a triangle with a blue interior. -->
        <Polygon Points="0,0 0,30 0,10 30,10 30,-10 45,10 30,30 30,20 0,20 0,0 30,0 30,10 0,10" Fill="Blue" Name="Polygon1" Canvas.Left="40" Canvas.Top="30" Canvas.ZIndex="40"/>
        <Polygon Points="0,0 0,30 0,10 30,10 30,-10 45,10 30,30 30,20 0,20 0,0 30,0 30,10 0,10" Fill="Green" Name="Polygon2" Canvas.Left="70" Canvas.Top="30" Canvas.ZIndex="30"/>
        <Polygon Points="0,0 0,30 0,10 30,10 30,-10 45,10 30,30 30,20 0,20 0,0 30,0 30,10 0,10" Fill="Red" Name="Polygon3" Canvas.Left="100" Canvas.Top="30" Canvas.ZIndex="20"/>
      </Canvas>
    </Window>

    并在事件处理程序中更改鼠标选定箭头的颜色和 ZIndex,并在窗口标题中反映选定的多边形名称。

    Clear-Host
    
    $polygon_data = @{}
    $reader = (New-Object System.Xml.XmlNodeReader $xaml)
    $target = [Windows.Markup.XamlReader]::Load($reader)
    $canvas = $target.FindName("Canvas1")
    function save_orig_design{
      param ([String] $name)
      $control = $target.FindName($name)
      return @{
          'fill'   =  ( $control.Fill.Color );
          'ZIndex' =  ( [System.Windows.Controls.Canvas]::GetZIndex($control) )
          }
      }
      $polygon_data['Polygon1'] = (save_orig_design('Polygon1'))
      $polygon_data['Polygon2'] = (save_orig_design('Polygon2'))
      $polygon_data['Polygon3'] = (save_orig_design('Polygon3'))
    
    # TODO :
    # $canvas.Add_Initialized ...
    function restore_orig {
      param ( [String] $name )
      $control = $target.FindName( $name )
      $color = [System.Windows.Media.ColorConverter]::ConvertFromString( [String] $polygon_data[$name]['fill'] )
      $control.Fill = new-Object System.Windows.Media.SolidColorBrush( $color )
      [System.Windows.Controls.Canvas]::SetZIndex($control, [Object] $polygon_data[$name]['ZIndex'])
    }
    $handler = {
    param (
        [Object]  $sender,
        [System.Windows.Input.MouseButtonEventArgs] $e  )
      @('Polygon1', 'Polygon2', 'Polygon3') | % { restore_orig( $_) }
      # Highlight sender
      $sender.Fill = new-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Colors]::Orange)
      # uncomment to reveal a distortion
      # $sender.Stroke = new-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Colors]::Black)
      # Bring sender to front
      [System.Windows.Controls.Canvas]::SetZIndex($sender,[Object]100)
      $target.Title="Hello $($sender.Name)"
    }
    foreach ($item in ('Polygon1', 'Polygon2', 'Polygon3') ){
      $control = $target.FindName($item)
      $eventMethod = $control.add_MouseDown
      $eventMethod.Invoke( $handler )
      $control = $null
     }
    $eventMethod.Invoke($handler)
    $target.ShowDialog() | out-null

    可以获得独特的效果。

    TreeView

    TreeView

    TreeView

    但是设计代码隐藏可能很困难。安排 PowerShell 和 WPF 之间的通信(well documented)并且看起来是一项相当具有挑战性的任务。

    连接 WPF 事件

    要安排 PowerShell 运行空间之间的交互,您可以创建一个可选的强类型 synchronized 对象,并创建一个额外的 RunSpace 来执行 WPF 事件。

    #requires -version 2
    $so = [hashtable]::Synchronized(@{
        'Result'  = '';
        'Window'  = [System.Windows.Window] $null ;
        'TextBox' = [System.Windows.Controls.TextBox] $null ;
        })
    $so.Result = ''
    $rs =[runspacefactory]::CreateRunspace()
    $rs.ApartmentState = 'STA'
    $rs.ThreadOptions = 'ReuseThread'
    $rs.Open()
    $rs.SessionStateProxy.SetVariable('so', $so)

    接下来,将 XAML 处理代码包装在 Add-Script 方法中。

    $run_script = [PowerShell]::Create().AddScript({
    
    Add-Type -AssemblyName PresentationFramework
    [xml]$xaml = @"
    <window height="100" title="Example with TextBox" width="300" x:name="Window" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <stackpanel height="100" width="300">
              <textblock fontsize="14" fontweight="Bold" text="A spell-checking TextBox:">
            <textbox acceptsreturn="True" acceptstab="True" fontsize="14" margin="5" spellcheck.isenabled="True" textwrapping="Wrap" x:name="textbox">
    
            </textbox>
    
      </textblock></stackpanel>
    </window>
    "@
    
    $reader = (New-Object System.Xml.XmlNodeReader $xaml)
    $target = [Windows.Markup.XamlReader]::Load( $reader )
    $so.Window  = $target
    $handler = {
        param (
        [Object]  $sender,
        [System.Windows.Controls.TextChangedEventArgs] $eventargs
        )
        $so.Result  = $sender.Text
    }
    $control = $target.FindName("textbox")
    $so.TextBox = $control
    
    $event = $control.Add_TextChanged
    $event.Invoke( $handler )
    
    $eventMethod.Invoke($handler)
    $target.ShowDialog() | out-null
    })

    然后设计通过共享对象 $so 操作的访问器函数。请注意,某些必须可访问的属性不能在另一个线程上进行评估。调用线程无法访问此对象,因为另一个线程拥有它,该异常仅在运行时引发。

    function send_text {
        Param (
            $content,
            [switch] $append
        )
        # WARNING - uncommenting the following line leads to exception
        # $so.Textbox = $so.Window.FindName("textbox")
    
        # NOTE - host-specific method signature:
        $so.Textbox.Dispatcher.invoke([System.Action]{
    
            if ($PSBoundParameters['append_content']) {
                $so.TextBox.AppendText($content)
            } else {
                $so.TextBox.Text = $content
            }
            $so.Result = $so.TextBox.Text
        }, 'Normal')
    }
    
    function close_dialog {
        $so.Window.Dispatcher.invoke([action]{
           $so.Window.Close()
        }, 'Normal')
    }

    最后,主脚本调用动态创建的脚本并控制窗体。

    $run_script.Runspace = $rs
    Clear-Host
    
    $data = $run_script.BeginInvoke()
    
    # TODO - synchronize properly
    
    start-sleep 1
    write-host $so.Result
    send_text -Content 'The qick red focks jumped over the lasy brown dog.'
    $cnt = 10
    [bool] $done = $false
    while (($cnt  -ne 0 ) -and -not $done) {
      write-output ('Text: {0} ' -f $so.Result )
      if ($so.Result -eq 'The quick red fox jumped over the lazy brown dog.' ){
        $done = $true;
      }
      else {
        start-sleep 10
      }
      $cnt --
    }
    close_dialog
    
    if ( -not $done ){
        write-output 'Time is up!'
    } else {
        write-output 'Well done!'
    }

    此示例初始化文本时包含一些拼写错误。

    TypeText1

    并等待用户修复拼写错误。一旦文本被更正或超时到期,窗体将关闭并打印摘要。

    TypeText2

    由于 PowerShell/WPF 通信所需的代码略微复杂,建议从更简单的示例开始,并在所有事件处理程序按预期执行后转换为最终形式。可以相对快速地以这种方式转换较早的示例。

    您还可以通过窗体安排双向通信,例如,在脚本的稍作修改的版本中将当前数据加载到复选框工具提示中。

    function Get-ScriptDirectory
    {
        $Invocation = (Get-Variable MyInvocation -Scope 1).Value;
        if($Invocation.PSScriptRoot)
        {
            $Invocation.PSScriptRoot;
        }
        Elseif($Invocation.MyCommand.Path)
        {
            Split-Path $Invocation.MyCommand.Path
        }
        else
        {
            $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
        }
    }
    $so = [hashtable]::Synchronized(@{
            'Result'  = [string] '';
            'ScriptDirectory'  = [string] '';
    
        'Window'  = [System.Windows.Window] $null ;
        'Control' = [System.Windows.Controls.ToolTip] $null ;
        'Contents' = [System.Windows.Controls.TextBox] $null ;
        'NeedData' = [bool] $false ;
        'HaveData' = [bool] $false ;
    
        })
    $so.ScriptDirectory = Get-ScriptDirectory
    
    $so.Result = ''
    $rs =[runspacefactory]::CreateRunspace()
    $rs.ApartmentState = 'STA'
    $rs.ThreadOptions = 'ReuseThread'
    $rs.Open()
    $rs.SessionStateProxy.SetVariable('so', $so)
    $run_script = [PowerShell]::Create().AddScript({
    
    Add-Type -AssemblyName PresentationFramework
    [xml]$xaml = @"
    <window height="190" removed="LightGray" title="About WPF" width="168" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
            <canvas>
    
            <img opacity=".7" source="$('{0}\{1}' -f $so.ScriptDirectory, 'clock.jpg' )" width="150" />
              <image.tooltip>
                <tooltip name="tooltip">
                <stackpanel>
                  <label background="Blue" fontweight="Bold" foreground="White">
                    The CheckBox
                  </label>
                  <stackpanel orientation="Horizontal">
                    <img margin="2" name="hourglass" source="$('{0}\{1}' -f $so.ScriptDirectory, 'hourglass.jpg' )" visibility="Collapsed" width="20" />
                  <textblock name="tooltip_textbox" padding="10" textwrapping="WrapWithOverflow" width="200">
                    please wait...
                  </textblock>
                  </stackpanel>
                </stackpanel>
               </tooltip>
              </image.tooltip>
    
            </canvas>
    </window>
    "@
    
    $reader = (New-Object System.Xml.XmlNodeReader $xaml)
    $target = [Windows.Markup.XamlReader]::Load($reader)
    $so.Window  = $target
    $control = $target.FindName("tooltip")
    $so.Indicator = $target.FindName("hourglass")
    $contents = $target.FindName("tooltip_textbox")
    $so.Control = $control
    $so.Contents = $contents
    $handler_opened = {
      param (
        [Object]  $sender,
        [System.Windows.RoutedEventArgs] $eventargs
        )
            $so.Contents.Text = 'please wait...'
            $so.Indicator.Visibility = 'Visible'
        $so.NeedData  = $true
            $so.Result = ''
    }
    $handler_closed = {
      param (
        [Object]  $sender,
        [System.Windows.RoutedEventArgs] $eventargs
        )
            $so.HaveData = $false
        $so.NeedData = $false
    }
    
    [System.Management.Automation.PSMethod] $event_opened = $control.Add_Opened
    [System.Management.Automation.PSMethod] $event_closed = $control.Add_Closed
    
    $event_opened.Invoke( $handler_opened )
    $event_closed.Invoke( $handler_closed)
    $target.ShowDialog() | out-null
    })
    
    function send_text {
        Param (
            $content,
            [switch] $append
        )
    
        # NOTE - host-specific method signature:
        $so.Indicator.Dispatcher.invoke([System.Action]{
            $so.Indicator.Visibility = 'Collapsed'
        }, 'Normal')
        $so.Contents.Dispatcher.invoke([System.Action]{
    
            if ($PSBoundParameters['append_content']) {
                $so.Contents.AppendText($content)
            } else {
                $so.Contents.Text = $content
            }
        $so.Result = $so.Contents.Text
        }, 'Normal')
    }
    
    $run_script.Runspace = $rs
    Clear-Host
    
    $handle = $run_script.BeginInvoke()
    While (-Not $handle.IsCompleted) {
        Start-Sleep -Milliseconds 100
        if ($so.NeedData -and -not $so.HaveData){
          write-output ('Need to provide data' )
          Start-Sleep -Milliseconds 10
          send_text -Content (Date)
          write-output ('Sent {0}' -f $so.Result )
          $so.HaveData = $true
        }
    }
    $run_script.EndInvoke($handle)
    $rs.Close()

    Toolbox1

    在此示例中,使用 ToolTip Opened,Closed 事件通过 SynchronizedNeedData 标志设置和清除到顶层脚本,然后将文本更改为 please wait 并显示沙漏直到数据准备好。数据渲染再次在 send_text 中执行。请注意,send_text 函数现在调用 Dispatcher 两次,视觉反馈不完美。每次鼠标离开和重新进入 Tooltip 激活区域时,都会请求并提供新数据。

    Toolbox2

    树视图

    纯文本

    TreeView

    在启动 PowerShell 脚本(例如,用于指标收集)时,通常需要从某个分组方式中选择特定的节点。

    function PromptTreeView
    {
         Param(
        [String] $title,
        [String] $message)
    
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections.Generic')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Collections')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.ComponentModel')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Text')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Data')
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
      $t = New-Object  System.Windows.Forms.TreeView
      $components = new-object System.ComponentModel.Container
      $f.SuspendLayout();
      $t.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);
    
      $i = new-Object System.Windows.Forms.ImageList($components)
      $i.Images.Add([System.Drawing.SystemIcons]::Application)
      $t.ImageList = $i
    
    
      $t.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
            -bor [System.Windows.Forms.AnchorStyles]::Left) `
            -bor [System.Windows.Forms.AnchorStyles]::Right)
      $t.ImageIndex = -1
      $t.Location = new-object System.Drawing.Point(4, 5)
      $t.Name = "treeFood"
      $t.SelectedImageIndex = -1
      $t.Size = new-object System.Drawing.Size(284, 256)
      $t.TabIndex = 1;
      $t_AfterSelect =  $t.add_AfterSelect
      $t_AfterSelect.Invoke({
        param(
        [Object] $sender,
        [System.Windows.Forms.TreeViewEventArgs] $eventargs
        )
        if ($eventargs.Action -eq [System.Windows.Forms.TreeViewAction]::ByMouse)
        {
            write-host $eventargs.Node.FullPath
        }
    })
    
     $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
     $f.ClientSize = new-object System.Drawing.Size(292, 266)
     $f.Controls.AddRange(@( $t))
     $f.Name = "TreeViewExample"
     $f.Text = "TreeView Example"
     $f_Load = $f.add_Load
     $f_Load.Invoke({
       param(
        [Object] $sender,
        [System.EventArgs] $eventargs
       )
    
        $node = $t.Nodes.Add("Fruits")
        $node.Nodes.Add("Apple")
        $node.Nodes.Add("Peach")
    
        $node = $t.Nodes.Add("Vegetables")
        $node.Nodes.Add("Tomato")
        $node.Nodes.Add("Eggplant")
    })
    
      $f.ResumeLayout($false)
    
      $f.Name = 'Form1'
      $f.Text = 'TreeView Sample'
      $t.ResumeLayout($false)
      $f.ResumeLayout($false)
      $f.StartPosition = 'CenterScreen'
      $f.KeyPreview = $false
    
      $f.Topmost = $True
      $caller = New-Object Win32Window -ArgumentList([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    
      $f.Add_Shown( { $f.Activate() } )
    
      [Void] $f.ShowDialog([Win32Window ] ($caller) )
    
      $t.Dispose()
      $f.Dispose()
    }

    高级

    自定义图标

    通过添加 ScriptDirectory 属性……

        private string _script_directory;
        public string ScriptDirectory
        {
            get { return _script_directory; }
            set { _script_directory = value; }
        }

    ……并更新 PromptTreeView 签名以接收 $caller,脚本可以通过 $caller 将其位置传递给窗体。

    $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    $caller.ScriptDirectory = Get-ScriptDirectory
    
    $result = PromptTreeView 'Items'  $caller
    
    function Get-ScriptDirectory
    {
    #  implementation omitted
    # http://stackoverflow.com/questions/8343767/how-to-get-the-current-directory-of-the-cmdlet-being-executed
    }

    后者将能够加载自定义图标。

       try {
         $script_path  = $caller.ScriptDirectory
        } catch [Exception] {
               # slurp the exception - debug code omitted
        }
        if ($script_path -eq '' -or $script_path -eq $null )  {
          $script_path = get-location
       }
      foreach ($n in @(1,2,3)){
         $image_path =  ( '{0}\color{1}.gif' -f $script_path ,  $n )
         $image = [System.Drawing.Image]::FromFile($image_path)
         $i.Images.Add($image)
      }

    TreeView

    并为各个节点使用不同的图标。使用相同的技术,调用脚本可以描述要为每个节点渲染的图标。

    $node = $t.Nodes.Add("Fruits")
        $apple = $node.Nodes.Add("Apple")
        $apple.ImageIndex  = 1
    
        $node.Nodes.Add("Peach")
    
        $node = $t.Nodes.Add("Vegetables")
        $tomato = $node.Nodes.Add("Tomato")
        $tomato.ImageIndex  = 2 

    后台工作者

    此脚本的下一个迭代还包含一个更详细的事件处理程序版本。该示例可用于处理在用户为远程位置提供对象(存在延迟)时可能需要的时间消耗型验证。在不强迫用户退出对话框的情况下进行此类验证可能是可取的。在下面的代码中,窗体 TreeView 元素单击会实例化一个 BackgroundWorker 以在单独的线程上处理操作。窗体目前不提供视觉提示 $worker 已启动,尽管这是可能的。

    因此,模态对话框仍然可以——由于事件处理代码是 100% PowerShell,因此无需安排脚本和窗体之间复杂的同步——每次窗体希望通过调用相关 PowerShell cmdlet 来运行数据验证时,它都可以直接进行。

    $worker = new-object System.ComponentModel.BackgroundWorker
        $worker.WorkerReportsProgress = $false;
        $worker.WorkerSupportsCancellation = $false;
        $worker_DoWork  = $worker.Add_DoWork
        $worker_DoWork.Invoke({
            param(
            [Object] $sender,
            [System.Windows.Forms.DoWorkEventArgs] $eventargs
        )
        })

    所有工作都在 Completed 事件处理程序中完成。在这个例子中,文本文件“etc/hosts”在记事本中打开,并且线程等待用户关闭记事本。这是 Windows.Forms 的标准示例/推荐实践,除了 Backgroundworker 通常用 C# 实现。很高兴发现它与 PowerShell 代码开箱即用。

    $worker_RunWorkerCompleted = $worker.Add_RunWorkerCompleted
        $worker_RunWorkerCompleted.Invoke({
            param(
            [Object] $sender,
            [System.ComponentModel.RunWorkerCompletedEventArgs] $eventargs
            )
           $child_proc  = [System.Diagnostics.Process]::Start('notepad',"$env:windir\system32\drivers\etc\hosts")
           $child_proc.WaitForExit()
    })

    制表

    人们真的希望将树视图安装到选项卡中,而不是文本框中。这将使选项选择完全由鼠标驱动,并且是可能的。

    与早期示例的微小区别是 treeview 重绘后的事件名称——对于 tabPage 它是 VisibleChangedEvent

    #
    $panel1.add_VisibleChanged({
       param(
        [Object]$sender,
        [System.EventArgs]$eventargs
       )
        $t1.SuspendLayout()
        $t1.Nodes.Clear()
        $node = $t1.Nodes.Add('Target Environment')
        $node.Nodes.Add('Database Server')
        $node.Nodes.Add('Application Server')
        $sites = $node.Nodes.Add('Web Server')
        $sites.Nodes.Add('Site 1')
        $sites.Nodes.Add('Site 2')
        $sites.Nodes.Add('Site 3')
        $t1.ResumeLayout($false)
        $t1.PerformLayout()
    })

    完整源代码如下。

    function TabsWithTreeViews(
        [String] $title,
      [Object] $caller
        ){
    
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
    
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
    
      $panel2 = new-object System.Windows.Forms.TabPage
      $panel1 = new-object System.Windows.Forms.TabPage
      $tab_contol1 = new-object System.Windows.Forms.TabControl
      $panel2.SuspendLayout()
      $panel1.SuspendLayout()
      $tab_contol1.SuspendLayout()
      $f.SuspendLayout()
    
      $panel2.Location = new-object System.Drawing.Point(4, 22)
      $panel2.Name = "tabPage2"
      $panel2.Padding = new-object System.Windows.Forms.Padding(3)
      $panel2.Size = new-object System.Drawing.Size(259, 352)
      $panel2.AutoSize  = $true
      $panel2.TabIndex = 1
      $panel2.Text = "Source Node"
    
      $l1 = New-Object System.Windows.Forms.Label
      $l1.Location = New-Object System.Drawing.Point(8,12)
      $l1.Size = New-Object System.Drawing.Size(220,16)
      $l1.Text = 'enter status message here'
    
      $l1.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 8, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);
    
      $groupBox1 = New-Object System.Windows.Forms.GroupBox
    
      $groupBox1.SuspendLayout()
    
      $groupBox1.Controls.AddRange( @($l1 ))
      $groupBox1.Location = New-Object System.Drawing.Point(8,230)
      $groupBox1.Name = 'groupBox1'
      $groupBox1.Size = New-Object System.Drawing.Size(244,32)
      $groupBox1.TabIndex = 0
      $groupBox1.TabStop = $false
      $groupBox1.Text = 'status'
    
    
      $panel2.Controls.Add($groupBox1)
    
      $t2 = New-Object  System.Windows.Forms.TreeView
      $t2.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);
    
      $i = new-Object System.Windows.Forms.ImageList($components)
      $i.Images.Add([System.Drawing.SystemIcons]::Application)
      $t2.ImageList = $i
    
      $t2.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
      -bor [System.Windows.Forms.AnchorStyles]::Left) `
      -bor [System.Windows.Forms.AnchorStyles]::Right)
      $t2.ImageIndex = -1
      $t2.Location = new-object System.Drawing.Point(4, 5)
      $t2.Name = "treeFood"
      $t2.SelectedImageIndex = -1
      $t2.Size = new-object System.Drawing.Size(284, 224)
    
      $t2.AutoSize  = $true
      $t2.TabIndex = 1;
      $panel2.Controls.AddRange(@($t2))
    
     # http://msdn.microsoft.com/en-us/library/system.windows.forms.tabpage.visiblechanged%28v=vs.110%29.aspx
       $panel2.add_VisibleChanged({
         param(
          [Object] $sender,
          [System.EventArgs] $eventargs
          )
        $t2.SuspendLayout()
        $t2.Nodes.Clear()
        $node = $t2.Nodes.Add('Source Environment')
        $server = $node.Nodes.Add('Test Server')
        $databases = $server.Nodes.Add('Databases')
        $server.Nodes.Add('DB 1')
        $server.Nodes.Add('DB 2')
    
        $server.Nodes.Add('Application')
        $sites = $server.Nodes.Add('IIS Web Sites')
    
        $sites.Nodes.Add('Site 1')
        $sites.Nodes.Add('Site 2')
        $sites.Nodes.Add('Site 3')
        $t2.ResumeLayout($false)
        $t2.PerformLayout()
    })
    
      $panel1.Location = new-object System.Drawing.Point(4, 22)
      $panel1.Name = "tabPage1"
      $panel1.Padding = new-object System.Windows.Forms.Padding(3)
      $panel1.Size = new-object System.Drawing.Size(259, 252)
      $panel1.TabIndex = 0
      $panel1.Text = "Destination Node"
    
      $t1 = New-Object  System.Windows.Forms.TreeView
      $t1.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);
    
      $t1.ImageList = $i
    
      $t1.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
      -bor [System.Windows.Forms.AnchorStyles]::Left) `
      -bor [System.Windows.Forms.AnchorStyles]::Right)
      $t1.ImageIndex = -1
      $t1.Location = new-object System.Drawing.Point(4, 5)
      $t1.Name = "treeFood"
      $t1.SelectedImageIndex = -1
      $t1.Size = new-object System.Drawing.Size(284, 224)
    
      $t1.AutoSize  = $true
      $t1.TabIndex = 1;
      $panel1.Controls.AddRange(@($t1))
    
       $panel1.add_VisibleChanged({
           param(
              [Object] $sender,
              [System.EventArgs] $eventargs
           )
        $t1.SuspendLayout()
        $t1.Nodes.Clear()
        $node = $t1.Nodes.Add('Target Environment')
        $node.Nodes.Add('Database Server')
        $node.Nodes.Add('Application Server')
        $sites = $node.Nodes.Add('Web Server')
        $sites.Nodes.Add('Site 1')
        $sites.Nodes.Add('Site 2')
        $sites.Nodes.Add('Site 3')
        $t1.ResumeLayout($false)
        $t1.PerformLayout()
    })
    
      $tab_contol1.Controls.Add($panel1)
      $tab_contol1.Controls.Add($panel2)
    
      $tab_contol1.Location = new-object System.Drawing.Point(13, 13)
      $tab_contol1.Name = "tabControl1"
      $tab_contol1.SelectedIndex = 1
      $tab_contol1.Size = new-object System.Drawing.Size(267, 288)
      $tab_contol1.TabIndex = 0
    
      $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
      $f.ClientSize = new-object System.Drawing.Size(292, 308)
      $f.Controls.Add($tab_contol1)
      $panel2.ResumeLayout($false)
      $panel2.PerformLayout()
      $panel1.ResumeLayout($false)
      $tab_contol1.ResumeLayout($false)
      $f.ResumeLayout($false)
    
      $f.Topmost = $true
    
    
      $f.Add_Shown( { $f.Activate() } )
      $f.KeyPreview = $True
    
    
      [Void] $f.ShowDialog([Win32Window ] ($caller) )
    
      $f.Dispose()
    }

    TreeView

    代码仍在进行中,目的是使用状态标签进行验证警告,并使用工作进程进行更深入的环境验证。

    TreeView

    下拉组合框

    要在 V4 之前的 PowerShell 环境中管理 PowerShell Desired State Configuration 配置管理器 - 节点 - 提供程序 - 属性输入,您可能希望使用 combobox 扩展 treeview 。例如,可以使用 Mattman206 的自定义带组合框下拉节点的树视图控件。在编译类并将程序集放置在 SHARED_ASSEMBLIES_PATH 文件夹后,脚本会加载它,并在表单加载事件处理过程中自由混合 System.Windows.Forms.TreeNodeDropDownTreeView.DropDownTreeNode 节点:Mattman206 可以这样做。在编译类并将程序集放置在 SHARED_ASSEMBLIES_PATH 文件夹后,脚本会加载它,

    制表

    人们真的希望将树视图安装到选项卡中,而不是文本框中。这将使选项选择完全由鼠标驱动,并且是可能的。

    与早期示例的微小区别是 treeview 重绘后的事件名称——对于 tabPage 它是 VisibleChangedEvent

    #
    $panel1.add_VisibleChanged({
       param(
        [Object]$sender,
        [System.EventArgs]$eventargs
       )
        $t1.SuspendLayout()
        $t1.Nodes.Clear()
        $node = $t1.Nodes.Add('Target Environment')
        $node.Nodes.Add('Database Server')
        $node.Nodes.Add('Application Server')
        $sites = $node.Nodes.Add('Web Server')
        $sites.Nodes.Add('Site 1')
        $sites.Nodes.Add('Site 2')
        $sites.Nodes.Add('Site 3')
        $t1.ResumeLayout($false)
        $t1.PerformLayout()
    })

    完整源代码如下。

    function TabsWithTreeViews(
        [String] $title,
      [Object] $caller
        ){
    
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
      [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')
    
      $f = New-Object System.Windows.Forms.Form
      $f.Text = $title
    
      $panel2 = new-object System.Windows.Forms.TabPage
      $panel1 = new-object System.Windows.Forms.TabPage
      $tab_contol1 = new-object System.Windows.Forms.TabControl
      $panel2.SuspendLayout()
      $panel1.SuspendLayout()
      $tab_contol1.SuspendLayout()
      $f.SuspendLayout()
    
      $panel2.Location = new-object System.Drawing.Point(4, 22)
      $panel2.Name = "tabPage2"
      $panel2.Padding = new-object System.Windows.Forms.Padding(3)
      $panel2.Size = new-object System.Drawing.Size(259, 352)
      $panel2.AutoSize  = $true
      $panel2.TabIndex = 1
      $panel2.Text = "Source Node"
    
      $l1 = New-Object System.Windows.Forms.Label
      $l1.Location = New-Object System.Drawing.Point(8,12)
      $l1.Size = New-Object System.Drawing.Size(220,16)
      $l1.Text = 'enter status message here'
    
      $l1.Font = new-object System.Drawing.Font('Microsoft Sans Serif', 8, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, 0);
    
      $groupBox1 = New-Object System.Windows.Forms.GroupBox
    
      $groupBox1.SuspendLayout()
    
      $groupBox1.Controls.AddRange( @($l1 ))
      $groupBox1.Location = New-Object System.Drawing.Point(8,230)
      $groupBox1.Name = 'groupBox1'
      $groupBox1.Size = New-Object System.Drawing.Size(244,32)
      $groupBox1.TabIndex = 0
      $groupBox1.TabStop = $false
      $groupBox1.Text = 'status'
    
    
      $panel2.Controls.Add($groupBox1)
    
      $t2 = New-Object  System.Windows.Forms.TreeView
      $t2.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);
    
      $i = new-Object System.Windows.Forms.ImageList($components)
      $i.Images.Add([System.Drawing.SystemIcons]::Application)
      $t2.ImageList = $i
    
      $t2.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
      -bor [System.Windows.Forms.AnchorStyles]::Left) `
      -bor [System.Windows.Forms.AnchorStyles]::Right)
      $t2.ImageIndex = -1
      $t2.Location = new-object System.Drawing.Point(4, 5)
      $t2.Name = "treeFood"
      $t2.SelectedImageIndex = -1
      $t2.Size = new-object System.Drawing.Size(284, 224)
    
      $t2.AutoSize  = $true
      $t2.TabIndex = 1;
      $panel2.Controls.AddRange(@($t2))
    
     # http://msdn.microsoft.com/en-us/library/system.windows.forms.tabpage.visiblechanged%28v=vs.110%29.aspx
       $panel2.add_VisibleChanged({
         param(
          [Object] $sender,
          [System.EventArgs] $eventargs
          )
        $t2.SuspendLayout()
        $t2.Nodes.Clear()
        $node = $t2.Nodes.Add('Source Environment')
        $server = $node.Nodes.Add('Test Server')
        $databases = $server.Nodes.Add('Databases')
        $server.Nodes.Add('DB 1')
        $server.Nodes.Add('DB 2')
    
        $server.Nodes.Add('Application')
        $sites = $server.Nodes.Add('IIS Web Sites')
    
        $sites.Nodes.Add('Site 1')
        $sites.Nodes.Add('Site 2')
        $sites.Nodes.Add('Site 3')
        $t2.ResumeLayout($false)
        $t2.PerformLayout()
    })
    
      $panel1.Location = new-object System.Drawing.Point(4, 22)
      $panel1.Name = "tabPage1"
      $panel1.Padding = new-object System.Windows.Forms.Padding(3)
      $panel1.Size = new-object System.Drawing.Size(259, 252)
      $panel1.TabIndex = 0
      $panel1.Text = "Destination Node"
    
      $t1 = New-Object  System.Windows.Forms.TreeView
      $t1.Font  = new-object System.Drawing.Font('Tahoma', 10.25, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);
    
      $t1.ImageList = $i
    
      $t1.Anchor = ((([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom) `
      -bor [System.Windows.Forms.AnchorStyles]::Left) `
      -bor [System.Windows.Forms.AnchorStyles]::Right)
      $t1.ImageIndex = -1
      $t1.Location = new-object System.Drawing.Point(4, 5)
      $t1.Name = "treeFood"
      $t1.SelectedImageIndex = -1
      $t1.Size = new-object System.Drawing.Size(284, 224)
    
      $t1.AutoSize  = $true
      $t1.TabIndex = 1;
      $panel1.Controls.AddRange(@($t1))
    
       $panel1.add_VisibleChanged({
           param(
              [Object] $sender,
              [System.EventArgs] $eventargs
           )
        $t1.SuspendLayout()
        $t1.Nodes.Clear()
        $node = $t1.Nodes.Add('Target Environment')
        $node.Nodes.Add('Database Server')
        $node.Nodes.Add('Application Server')
        $sites = $node.Nodes.Add('Web Server')
        $sites.Nodes.Add('Site 1')
        $sites.Nodes.Add('Site 2')
        $sites.Nodes.Add('Site 3')
        $t1.ResumeLayout($false)
        $t1.PerformLayout()
    })
    
      $tab_contol1.Controls.Add($panel1)
      $tab_contol1.Controls.Add($panel2)
    
      $tab_contol1.Location = new-object System.Drawing.Point(13, 13)
      $tab_contol1.Name = "tabControl1"
      $tab_contol1.SelectedIndex = 1
      $tab_contol1.Size = new-object System.Drawing.Size(267, 288)
      $tab_contol1.TabIndex = 0
    
      $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
      $f.ClientSize = new-object System.Drawing.Size(292, 308)
      $f.Controls.Add($tab_contol1)
      $panel2.ResumeLayout($false)
      $panel2.PerformLayout()
      $panel1.ResumeLayout($false)
      $tab_contol1.ResumeLayout($false)
      $f.ResumeLayout($false)
    
      $f.Topmost = $true
    
    
      $f.Add_Shown( { $f.Activate() } )
      $f.KeyPreview = $True
    
    
      [Void] $f.ShowDialog([Win32Window ] ($caller) )
    
      $f.Dispose()
    }

    TreeView

    代码仍在进行中,目的是使用状态标签进行验证警告,并使用工作进程进行更深入的环境验证。

    TreeView

    制表项的树

    下一个示例使用了漂亮的 TreeTabControl。制表项树 来用于 PowerShell。

    可以在 TreeTab/TreeTab/TreeTabControl.xaml.cs 类中添加一个小的公共方法,使 PowerShell 可以使用该类。

            /// <summary>
            /// Converts the string parameter to TreeItemType enumeration.
            /// </summary>
            /// <param name="_typestring">string</param>
            /// <returns>_type</returns>
            public TreeItem.TREEITEM_TYPE ConvertType(string _typestring ){
             TreeItem.TREEITEM_TYPE _type;
                if (String.Compare(_typestring, "MAIN", true) == 0)
                	_type = TreeItem.TREEITEM_TYPE.MAIN;         
                else
                	_type = TreeItem.TREEITEM_TYPE.GROUP;
                return _type;
            }

    因为

     public enum TREEITEM_TYPE
            {
                MAIN,
                GROUP
            }

    对 PowerShell 不可访问。

    您几乎不修改原始容器 XAML。

    <?xml version="1.0"?>
    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:custom="clr-namespace:TreeTab;assembly=TreeTab" Title="Window1" Margin="0,0,0,0" Height="244" Width="633">
      <Grid x:Name="Container">
        <Grid.RowDefinitions>
          <RowDefinition Height="30"/>
          <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid>
          <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
          </Grid.ColumnDefinitions>
          <Button x:Name="Hide_Tree" Grid.Column="1">Hide Tree</Button>
          <Button x:Name="Show_Tree" Grid.Column="0">Show Tree</Button>
        </Grid>
        <Grid x:Name="Container2" Grid.Row="1" Margin="5,5,5,5">
          <StackPanel x:Name="TreeTabContainer"></StackPanel>
        </Grid>
      </Grid>
    </Window>

    TreeView

    PowerShell 脚本初始化了管道代码。

    $shared_assemblies = @(
      'TreeTab.dll',
      'nunit.framework.dll'
    )
    [void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
    
    $shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'
    
    if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
      $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
    }
    
    pushd $shared_assemblies_path
    $shared_assemblies | ForEach-Object { Unblock-File -Path $_; Add-Type -Path $_ }
    popd
    
    
    Clear-Host
    
    
    $reader = (New-Object System.Xml.XmlNodeReader $xaml)
    $target = [Windows.Markup.XamlReader]::Load($reader)

    编译类并将程序集放置在 SHARED_ASSEMBLIES_PATH 文件夹后,将 TreeTab.TreeTabControl 的实例放置在 StackPanel 中。

    $t = New-Object -TypeName 'TreeTab.TreeTabControl'
    
    $c = $target.FindName('TreeTabContainer')
    $t.IsTreeExpanded = $true
    $t.Name = 'treeTab'
    [void]$t.HideTree()
    [void]$t.AddTabItem('Global','Global',$false,$t.ConvertType('MAIN'),'')
    [void]$t.AddTabItem('Staging_Environment','Staging Environment',$false,$t.ConvertType('GROUP'),'')
    [void]$t.AddTabItem('Test_Environment','Test Environment',$false,$t.ConvertType($t.ConvertType('GROUP')),'')
    
    [TreeTab.TreeTabItemGroup]$tp0 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('Staging_Environment')
    [TreeTab.TreeTabItem]$tItem = $t.AddTabItem('Certificates','Certificates',$false,$t.ConvertType('MAIN'),$tp0)
    [void]$t.AddTabItem('IIS_Web_Sites','IIS Web Sites',$false,$t.ConvertType('GROUP'),$tp0)
    [void]$t.AddTabItem('Databases','Databases',$false,$t.ConvertType('GROUP'),$tp0)
    
    [TreeTab.TreeTabItemGroup]$tp02 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('Databases')
    [void]$t.AddTabItem('DB_1','DB 1',$true,$t.ConvertType('MAIN'),$tp02)
    [void]$t.AddTabItem('DB_2','DB 2',$true,$t.ConvertType('MAIN'),$tp02)
    
    [TreeTab.TreeTabItemGroup]$tp03 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('IIS_Web_Sites')
    [void]$t.AddTabItem('Site_1','Site 1',$true,$t.ConvertType('MAIN'),$tp03)
    [void]$t.AddTabItem('Site_2','Site 2',$true,$t.ConvertType('MAIN'),$tp03)
    [void]$t.AddTabItem('Site_3','Site 3',$true,$t.ConvertType('MAIN'),$tp03)
    [void]$t.AddTabItem('Site_4','Site 4',$true,$t.ConvertType('MAIN'),$tp03)
    
    
    [TreeTab.TreeTabItemGroup]$tp01 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('Test_Environment')
    [TreeTab.TreeTabItem]$t23 = $t.AddTabItem('Certificates1','Certificates',$false,$t.ConvertType('MAIN'),$tp01)
    [void]$t.AddTabItem('IIS_Web_Sites2','IIS Web Sites',$false,$t.ConvertType('GROUP'),$tp01)
    [void]$t.AddTabItem('Databases2','Databases',$false,$t.ConvertType('GROUP'),$tp01)
    
    
    [TreeTab.TreeTabItemGroup]$tp12 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('Databases2')
    [void]$t.AddTabItem('DB_11','DB 1',$true,$t.ConvertType('MAIN'),$tp12)
    [void]$t.AddTabItem('DB_12','DB 2',$true,$t.ConvertType('MAIN'),$tp12)
    
    [TreeTab.TreeTabItemGroup]$tp13 = [TreeTab.TreeTabItemGroup]$t.GetTabItemById('IIS_Web_Sites2')
    [void]$t.AddTabItem('Site_11','Site 1',$true,$t.ConvertType('MAIN'),$tp13)
    [void]$t.AddTabItem('Site_12','Site 2',$true,$t.ConvertType('MAIN'),$tp13)
    [void]$t.AddTabItem('Site_13','Site 3',$true,$t.ConvertType('MAIN'),$tp13)
    [void]$t.AddTabItem('Site_14','Site 4',$true,$t.ConvertType('MAIN'),$tp13)
    
    
    [void]$t.ShowTree()
    [void]$c.AddChild($t)
    
    $target.FindName("Hide_Tree").add_click.Invoke({
        [void]$t.HideTree()
      })
    $target.FindName("Show_Tree").add_click.Invoke({
        [void]$t.ShowTree()
      })
    
    $target.ShowDialog() | Out-Null

    该类自动化了标签导航。接下来是填充选项卡标准 WPF 输入并提供特定于领域的 PING。

    例如,给定

    [xml]$parent_markup = @"

    <pre lang="xml">
    <?xml version="1.0"?>
    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Margin="5,5,5,5" Height="310" Width="420">
    <ScrollViewer>
        <WrapPanel>
      <Grid x:Name="LayoutRoot">
      </Grid>
         </WrapPanel>
      </ScrollViewer>
    </Window>

    "@

    [xml]$child_markup = @"

    <?xml version="1.0"?>
    <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
      <StackPanel.Resources>
        <Style TargetType="{x:Type TextBox}">
          <Setter Property="Margin" Value="0,10,0,0"/>
        </Style>
      </StackPanel.Resources>
      <Label x:Name="lblNumberOfTargetHits" HorizontalAlignment="Center">Input:</Label>
      <TextBox Width="120" x:Name="txtTargetKeyFocus" FontSize="12"/>
      <TextBox x:Name="txtTargetFocus" TextWrapping="Wrap" FontSize="12"/>
    </StackPanel>

    "@

    嵌套控件的实现方式与

    $parent_reader = (New-Object System.Xml.XmlNodeReader $parent_markup)
    $parent_target = [Windows.Markup.XamlReader]::Load($parent_reader)
    $LayoutRoot = $parent_target.FindName("LayoutRoot")
    $child_reader = (New-Object System.Xml.XmlNodeReader $child_markup)
    $child_target = [Windows.Markup.XamlReader]::Load($child_reader)
    $LayoutRoot.add_Loaded.Invoke({
        $LayoutRoot.Children.Add($child_target)
      })

    要运行 WPF 控件事件处理程序中的代码,请确保控件可以通过其标记 x:Name 属性找到,例如 $child,而不是 $parent

    $target = $child_target
    $control = $target.FindName("txtTargetKeyFocus")
    
    $handler_got_keyboard_focus = {
      param(
        [object]$sender,
        [System.Windows.Input.KeyboardFocusChangedEventArgs]$e
      )
      $source = $e.Source
      $source.Background = [System.Windows.Media.Brushes]::LightBlue
      $source.Clear()
    
    }
    $handler_lost_keyboard_focus = {
      param(
        [object]$sender,
        [System.Windows.Input.KeyboardFocusChangedEventArgs]$e
      )
      $source = $e.Source
      $source.Background = [System.Windows.Media.Brushes]::White
    }
    
    [System.Management.Automation.PSMethod]$event_got_keyboard_focus = $control.Add_GotKeyboardFocus
    [System.Management.Automation.PSMethod]$event_lost_keyboard_focus = $control.Add_LostKeyboardFocus
    
    $event_got_keyboard_focus.Invoke($handler_got_keyboard_focus)
    $event_lost_keyboard_focus.Invoke($handler_lost_keyboard_focus)
    $control = $null

    继续处理其余控件。

    注意:借助 System.Management.Automation.TypeAccelerators 程序集,您可以避免在脚本中键入完整的类名。

    $ta = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
    
    Add-Type -AssemblyName 'PresentationCore','PresentationFramework' -Passthru |
    Where-Object IsPublic |
    ForEach-Object {
      $_class = $_
      try {
        $ta::Add($_class.Name,$_class)
      } catch {
        ( 'Failed to add {0} accelerator resolving to {1}' -f $_class.Name ,   $_class.FullName )
      }
    }

    借助上面的代码,以下片段

    # http://poshcode.org/5730
    [Window]@{
      Width = 310
      Height = 110
      WindowStyle = 'SingleBorderWindow'
      AllowsTransparency = $false
      TopMost = $true
      Content = & {
        $c1 = [StackPanel]@{
          Margin = '5'
          VerticalAlignment = 'Center'
          HorizontalAlignment = 'Center'
          Orientation='Horizontal'
        }
    
     $t = [textblock]@{}
    $t.AddChild([label]@{
    Margin = '5'
    VerticalAlignment = 'Center'
    HorizontalAlignment = 'Center'
    FontSize = '11'
    FontFamily = 'Calibri'
    Foreground = 'Black'
    Content = 'Enter Password:'
    }
    )
    $c1.AddChild($t)
    $c1.AddChild(
    [passwordbox]@{
    Name = 'passwordBox'
    PasswordChar = '*'
    VerticalAlignment = 'Center'
    Width = '120'
    }
    )
    $c1.AddChild(
    [button]@{
    Content = 'OK'
    IsDefault = 'True'
    Margin = '5'
    Name = 'button1'
    Width = '50'
    VerticalAlignment = 'Center'
    }
    )
    ,$c1} | ForEach-Object {
      $_.Add_MouseLeftButtonDown({
          $this.DragMove()
        })
      $_.Add_MouseRightButtonDown({
          $this.Close()
        })
      $_.ShowDialog() | Out-Null
    }

    产生类似的效果

    <?xml version="1.0"?>
    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Margin="5,5,5,5" Height="110" Width="310">
    
     <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Orientation="Horizontal"  VerticalAlignment="Center" HorizontalAlignment = "Center">
       <TextBlock Margin="5" FontSize = "11" FontFamily = "Calibri">
                 Enter Password:
             </TextBlock>
       <PasswordBox Name="passwordBox" PasswordChar="*" VerticalAlignment="Center" Width="120"/>
       <Button Content="OK" IsDefault="True" Margin="5" Name="button1"  Width="50" VerticalAlignment="Center"/>
     </StackPanel>
    </Window>

    在大多数情况下,这不会在事件处理程序中产生歧义。

    系统托盘通知图标

    假设脚本正在运行一系列步骤,具有详细的日志记录,并且需要很长时间才能完成。自然地,您可以生成一个 Windows 系统托盘通知图标,该图标会指示正在进行的进程在做什么。关键是如何组织代码,以便控件保留在主脚本中。

    通过最少的修改,ScriptIT 提供的系统托盘通知图标示例,您可以让主脚本通过气球提示消息和控制台显示其状态,并且构建日志文件用于渲染托盘图标菜单并向其传递其他信息。

    TreeView

    #requires -version 2
    Add-Type -AssemblyName PresentationFramework
    
    function Get-ScriptDirectory
    {
        $Invocation = (Get-Variable MyInvocation -Scope 1).Value;
        if($Invocation.PSScriptRoot)
        {
            $Invocation.PSScriptRoot;
        }
        Elseif($Invocation.MyCommand.Path)
        {
            Split-Path $Invocation.MyCommand.Path
        }
        else
        {
            $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
        }
    }
    $so = [hashtable]::Synchronized(@{
            'Result'  = [string] '';
            'ConfigFile'  = [string] '';
            'ScriptDirectory'  = [string] '';
        'Form'  = [System.Windows.Forms.Form] $null ;
        'NotifyIcon' = [System.Windows.Controls.ToolTip] $null ;
        'ContextMenu' = [System.Windows.Forms.ContextMenu] $null ;
        })
    $so.ScriptDirectory = Get-ScriptDirectory
    $so.Result = ''
    $rs =[runspacefactory]::CreateRunspace()
    $rs.ApartmentState = 'STA'
    $rs.ThreadOptions = 'ReuseThread'
    $rs.Open()
    $rs.SessionStateProxy.SetVariable('so', $so)
    
    $run_script = [PowerShell]::Create().AddScript({
    
    [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
    
    $f = New-Object System.Windows.Forms.Form
    $so.Form  = $f
    $notify_icon = New-Object System.Windows.Forms.NotifyIcon
    $so.NotifyIcon = $notify_icon
    $context_menu = New-Object System.Windows.Forms.ContextMenu
    $exit_menu_item = New-Object System.Windows.Forms.MenuItem
    $AddContentMenuItem = New-Object System.Windows.Forms.MenuItem
    
    $build_log = ('{0}\{1}' -f $so.ScriptDirectory, 'build.log' )
    
    function Read-Config {
      $context_menu.MenuItems.Clear()
      if(Test-Path $build_log){
        $ConfigData = Get-Content $build_log
        $i = 0
        foreach($line in $ConfigData){
          if($line.Length -gt 0){
            $line = $line.Split(",")
            $Name = $line[0]
            $FilePath = $line[1]
            # Powershell style function invocation syntax
            $context_menu | Build-ContextMenu -index $i -text $Name -Action $FilePath
            $i++
          }
        }
      }
    
    # Create an Exit Menu Item
    $exit_menu_item.Index = $i+1
    $exit_menu_item.Text = 'E&xit'
    $exit_menu_item.add_Click({
    $f.Close()
    $notify_icon.visible = $false
    })
    
    $context_menu.MenuItems.Add($exit_menu_item) | Out-Null
    }
    
    function new-scriptblock([string]$textofscriptblock)
    {
    $executioncontext.InvokeCommand.NewScriptBlock($textofscriptblock)
    }
    
    # construct objects from the build log file and fill the context Menu
    function Build-ContextMenu {
      param (
            [int]$index = 0,
            [string]$Text,
            [string] $Action
      )
    begin
    {
    $menu_item = New-Object System.Windows.Forms.MenuItem
    }
    process
    {
    # Assign the Context Menu Object from the pipeline to the ContexMenu var
    $ContextMenu = $_
    }
    end
    {
    # Create the Menu Item$menu_item.Index = $index
    $menu_item.Text = $Text
    $scriptAction = $(new-scriptblock "Invoke-Item $Action")
    $menu_item.add_Click($scriptAction)
    $ContextMenu.MenuItems.Add($menu_item) | Out-Null
    }
    }
    # http://bytecookie.wordpress.com/2011/12/28/gui-creation-with-powershell-part-2-the-notify-icon-or-how-to-make-your-own-hdd-health-monitor/
    
    $notify_icon.Icon = ('{0}\{1}' -f $so.ScriptDirectory, 'sample.ico' )
    #
    $notify_icon.Text = 'Context Menu Test'
    # Assign the Context Menu
    $notify_icon.ContextMenu = $context_menu
    $f.ContextMenu = $context_menu
    
    # Control Visibility and state of things
    $notify_icon.Visible = $true
    $f.Visible = $false
    $f.WindowState = 'minimized'
    $f.ShowInTaskbar = $false
    $f.add_Closing({ $f.ShowInTaskBar = $False })
    $context_menu.Add_Popup({Read-Config})
    $f.ShowDialog()
    })
    
    function send_text {
        Param (
            [String] $title = 'script',
            [String] $message,
            [int]    $timeout = 10 ,
            [switch] $append
        )
    
        $so.NotifyIcon.ShowBalloonTip($timeout, $title , $message, [System.Windows.Forms.ToolTipIcon]::Info)
        write-output  -InputObject  ( '{0}:{1}' -f $title,  $message)
    }
    
    # -- main program --
    clear-host
    $run_script.Runspace = $rs
    
    $cnt = 0
    $total = 4
    $handle = $run_script.BeginInvoke()
    
    start-sleep 1
    
    send_text -title 'script' -message 'Starting...' -timeout 10
    $so.ConfigFile = $build_log = ('{0}\{1}' -f $so.ScriptDirectory, 'build.log' )
    set-Content -path $build_log -value ''
    
    While (-Not $handle.IsCompleted -and $cnt -lt $total) {
      start-sleep -Milliseconds 10000
      $cnt ++
      send_text -title 'script' -message ("Finished {0} of {1} items..."  -f $cnt, $total  )  -timeout 10
      write-output ("Subtask {0} ..." -f $cnt ) | out-file -FilePath $build_log -Append -encoding ascii
    }
    
    $so.Form.Close()
    
    $run_script.EndInvoke($handle) | out-null
    $rs.Close()
    write-output 'All finished'

    TreeView

    Selenium 测试

    下一个示例演示了如何从 PowerShell 执行 Selenium WebDriver 事务。这个示例还有很多代码需要添加,但已经开发的部分Hopefully 是值得一看的。这里选择了一个简单的事务进行说明。它已从以下 MS Test 示例转换而来。

    using System;
    using System.Linq.Expressions;
    using System.Text;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Microsoft.Activities.UnitTesting;
    using Moq;
    
    using OpenQA.Selenium;
    using OpenQA.Selenium.Remote;
    using OpenQA.Selenium.Firefox;
    using OpenQA.Selenium.Support.UI;
    using OpenQA.Selenium.IE;
    using OpenQA.Selenium.PhantomJS;
    using OpenQA.Selenium.Safari;
    namespace SeleniumTests
    {
        [TestClass]
        public class SeleniumTest
        {
    
        private static IWebDriver driver;
        private static StringBuilder verificationErrors = new StringBuilder();
            private string baseURL;
            private bool acceptNextAlert = true;
    
            [ClassCleanup()]
            public static void MyClassCleanup() {
                try {
            driver.Quit();
                } catch (Exception) {
                    // Ignore errors if unable to close the browser
                }
                Assert.AreEqual("", verificationErrors.ToString());
            }
    
            [TestInitialize()]
            public void MyTestInitialize()
            {
             // DesiredCapabilities capability = DesiredCapabilities.PhantomJSDriver();
             // error CS0117: 'OpenQA.Selenium.Remote.DesiredCapabilities' dos not contain a definition for 'PhantomJSDriver'
             // DesiredCapabilities capability = DesiredCapabilities.Firefox();
             // driver = new RemoteWebDriver(new Uri("http://127.0.0.1:4444/wd/hub"), capability );
             // driver = new PhantomJSDriver();
             driver = new SafariDriver();
             Assert.IsNotNull(driver );
    
             driver.Url =  baseURL = "http://www.wikipedia.org";
             driver.Manage().Timeouts().ImplicitlyWait( TimeSpan.FromSeconds(10 )) ;
             verificationErrors = new StringBuilder();
    
            }
    
            [TestCleanup()]
            public void MyTestCleanup() {
    
        }
    
            [TestMethod]
            public void Test()
            {
    
                // Arrange
                driver.Navigate().GoToUrl(baseURL + "/");
                WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)) ;
    
                // Act
                IWebElement queryBox = driver.FindElement(By.Id("searchInput"));
                queryBox.Clear();
                queryBox.SendKeys("Selenium");
            queryBox.SendKeys(Keys.ArrowDown);
                queryBox.Submit();
                driver.FindElement(By.LinkText("Selenium (software)")).Click();
                // Assert
                Assert.IsTrue(driver.Title.IndexOf("Selenium (software)") > -1, driver.Title);
            }
        }
    }

    而这反过来又基本上是一个带有 MS Test 装饰的Selenium IDE 记录。

    使用与本文档中其他示例类似的方法将 PowerShell 转换为 PowerShell——主要通过查阅 API 文档

    该脚本使用 PhantomeJS Selenium 驱动程序进行快速测试运行,并使用真实的 Firefox 浏览器进行彻底运行。

    所有标准的 Selenium C# 客户端 API dll 都放置在 SHARED_ASSEMBLIES_PATH 环境指向的文件夹中。

    $shared_assemblies =  @(
        'WebDriver.dll',
        'WebDriver.Support.dll',
        'Selenium.WebDriverBackedSelenium.dll',
        'Moq.dll'
    )
    
    $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
    pushd $shared_assemblies_path
    $shared_assemblies | foreach-object { Unblock-File -Path $_ ; Add-Type -Path  $_ }
    popd

    当然,如果存在业务逻辑层或封装低级 WebDriver 调用的 DSL,则可以从 C# 编译为独立的程序集 DLL,并以类似的方式提供给 PowerShell。

    $testSuite = [System.Reflection.AssemblyName]::GetAssemblyName('${assembly_path}\BusinessTestSuite.dll')
    
    $framework = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom(
    '${assembly_path}\BusinessSpecificWebDriverFramework.dll')

    为避免复制 Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll,而是从安装它的机器加载,并使熟悉的 断言调用在脚本中可用,以下代码执行快速发现。为简单起见,只显示 Microsoft Test Agent InstallLocation 注册表键扫描,需要尝试其他键,请注意 Visual Studio Express Edition 不安装此 dll,而 Enterprise 版本会安装几个副本。

    function read_registry{
      param ([string] $registry_path,
             [string] $package_name
    
    )
    
    pushd HKLM:
    cd -path $registry_path
    $settings = get-childitem -Path . | where-object { $_.Property  -ne $null } | where-object {$_.name -match  $package_name } |   select-object -first 1
    $values = $settings.GetValueNames()
    
    if ( -not ($values.GetType().BaseType.Name -match  'Array' ) ) {
      throw 'Unexpected result type'
    }
    $result = $null
    $values | where-object {$_ -match 'InstallLocation'} | foreach-object {$result = $settings.GetValue($_).ToString() ; write-debug $result}
    
    popd
    $result
    
    }
    
    $shared_assemblies =  @(
        'Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll'
    )
    
    $shared_assemblies_path = (  "{0}\{1}" -f   ( read_registry -registry_path '/HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersion/Uninstall' -package_name '{6088FCFB-2FA4-3C74-A1D1-F687C5F14A0D}' ) , 'Common7\IDE\PublicAssemblies' )
    $shared_assemblies_path =
    pushd $shared_assemblies_path
    $shared_assemblies | foreach-object { Unblock-File -Path $_ ; Add-Type -Path  $_ }
    popd
    
    [Microsoft.VisualStudio.TestTools.UnitTesting.Assert]::AreEqual("true", (@('true','false') | select-object -first 1) )

    根据开关,脚本初始化 phantom 或真实浏览器驱动……

    if ($PSBoundParameters['browser']) {
    
      Try {
        $connection = (New-Object Net.Sockets.TcpClient)
        $connection.Connect('127.0.0.1',4444)
        $connection.Close()
        }
      catch {
        $selemium_driver_folder = 'c:\java\selenium'
        start-process -filepath 'C:\Windows\System32\cmd.exe' -argumentlist "start cmd.exe /c ${selemium_driver_folder}\hub.cmd"
        start-process -filepath 'C:\Windows\System32\cmd.exe' -argumentlist "start cmd.exe /c ${selemium_driver_folder}\node.cmd"
        start-sleep 10
      }
    
      $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Firefox()
      $uri = [System.Uri]('http://127.0.0.1:4444/wd/hub')
      $driver = new-object OpenQA.Selenium.Remote.RemoteWebDriver($uri , $capability)
    } else {
      $phantomjs_executable_folder = 'C:\tools\phantomjs'
      $driver = new-object OpenQA.Selenium.PhantomJS.PhantomJSDriver($phantomjs_executable_folder)
      $driver.Capabilities.SetCapability('ssl-protocol', 'any' );
      $driver.Capabilities.SetCapability('ignore-ssl-errors', $true);
      $driver.capabilities.SetCapability("takesScreenshot", $false );
      $driver.capabilities.SetCapability("userAgent",
                  "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.7 Safari/534.34")
    }

    无需显式启动 PhantomJS 驱动程序。

    最后,测试开始(Get-ScriptDirectory 和 Assert 的实现未显示,可以在附加的源 zip 和作者的 github 存储库中找到)。

    # http://selenium.googlecode.com/git/docs/api/dotnet/index.html
    [void]$driver.Manage().Timeouts().ImplicitlyWait( [System.TimeSpan]::FromSeconds(10 ))
    [string]$baseURL = $driver.Url = 'http://www.wikipedia.org';
    $driver.Navigate().GoToUrl(('{0}/' -f $baseURL ))
    [OpenQA.Selenium.Remote.RemoteWebElement]$queryBox = $driver.FindElement([OpenQA.Selenium.By]::Id('searchInput'))
    
    $queryBox.Clear()
    $queryBox.SendKeys('Selenium')
    $queryBox.SendKeys([OpenQA.Selenium.Keys]::ArrowDown)
    $queryBox.Submit()
    $driver.FindElement([OpenQA.Selenium.By]::LinkText('Selenium (software)')).Click()
    $title =  $driver.Title
    assert -Script { ($title.IndexOf('Selenium (software)') -gt -1 ) } -message $title

    假定测试失败,脚本会导航到标识浏览器的 URL 并截屏。

    $driver.Navigate().GoToUrl("https://www.whatismybrowser.com/")
    [OpenQA.Selenium.Screenshot]$screenshot = $driver.GetScreenshot()
    $screenshot_path = $env:SCREENSHOT_PATH
    $screenshot.SaveAsFile(('{0}\{1}' -f $screenshot_path, 'a.png' ), [System.Drawing.Imaging.ImageFormat]::Png)

    Browser

    并完成测试运行。

    try {
      $driver.Quit()
    } catch [Exception] {
      # Ignore errors if unable to close the browser
    }

    您可能会通过适当的 CreateRunspace 调用引入一个单独的脚本,并开发 Synchronized 对象以允许从连接到主脚本的单独 PowerShell 运行空间控制 $driver.GetScreenshot 调用的调用(这目前正在进行中),方式类似于先前示例中控制的系统托盘通知图标。

    脚本的 Selenium RC 版本将加载不同的库并切换到 NunitAsserts

    $shared_assemblies =  @(
        'ThoughtWorks.Selenium.Core.dll',
        'nunit.core.dll',
        'nunit.framework.dll'
    )

    并调用不同的方法。

    $verificationErrors = new-object System.Text.StringBuilder
    $selenium = new-object Selenium.DefaultSelenium('localhost', 4444, '*firefox', 'http://www.wikipedia.org/')
    $selenium.Start()
    $selenium.Open('/')
    $selenium.Click('css=strong')
    $selenium.WaitForPageToLoad('30000')
    $selenium.Type('id=searchInput', 'selenium')
    $selenium.Click('id=searchButton')
    $selenium.WaitForPageToLoad('30000')
    $selenium.Click('link=Selenium (software)')
    $selenium.WaitForPageToLoad('30000')

    脚本的其余部分将保持不变。

    RC Driver called from Powershell

    当然,您可以在 PowerShell ISE 中直接创建脚本,这将节省大量开发时间。

    TreeView

    要使用最新版本的 Firefox(例如 33),您需要确保加载 特定版本的 Selenium C# 库——对 Nunit 访问 StringAssert 来说,类似的 버전 检查也很重要。

    $shared_assemblies = @{
      'WebDriver.dll' = 2.44;
      'WebDriver.Support.dll' = '2.44';
      'nunit.core.dll' = $null;
      'nunit.framework.dll' = '2.6.3';
    
    }
    
    $shared_assemblies.Keys | ForEach-Object {
      $assembly = $_
      $assembly_path = [System.IO.Path]::Combine($shared_assemblies_path,$assembly)
      $assembly_version = [Reflection.AssemblyName]::GetAssemblyName($assembly_path).Version
      $assembly_version_string = ('{0}.{1}' -f $assembly_version.Major,$assembly_version.Minor)
      if ($shared_assemblies[$assembly] -ne $null) {
     
        if (-not ($shared_assemblies[$assembly] -match $assembly_version_string)) {
          Write-Output ('Need {0} {1}, got {2}' -f $assembly,$shared_assemblies[$assembly],$assembly_path)
          Write-Output $assembly_version
          throw ('invalid version :{0}' -f $assembly)
        }
      }
    
      if ($host.Version.Major -gt 2) {
        Unblock-File -Path $_;
      }
      Write-Debug $_
      Add-Type -Path $_
    }
    popd

    一个非常有潜力增强的方面是处理文件下载对话框或多选项 Internet Explorer 警报弹出窗口。这些纯 Selenium 支持不佳。要么需要在测试框架中捆绑单独的工具,例如 Autoit,要么需要采用许多解决方法——后者有时感觉有点奇怪。

    当 Selenium 测试由 PowerShell 执行时,您可以包含一个从 C# 调用 win32 API 的类,并使用 EnumWindowsGetWindowInfoEnumPropsExGetPropGetWindowTextGetWindowTextLengthGetWindowThreadProcessId win32 API 从 user32.dll 通过 [DllImport()] 并加载 Windows.h 中定义的许多必需结构来访问窗口句柄并调用 PostMessageSendMessage 到所需的按钮,或者简单地调用 CloseWindow 到通过标题找到的 Alert / File Download 对话框。后者会导致一个测试失败,但会阻止整个测试套件在浏览器失去鼠标焦点后挂起。这在网络上的几个资源中得到了解释。

    并“另存为”对话框通过向其发送 WM_CLOSE Windows 消息来关闭。

    通过一点 P/invoke

        [DllImport("user32.dll")]
        public static extern Int32 SendMessage(IntPtr hwnd, UInt32 Msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPStr)] string lParam);
    
        [return: MarshalAs(UnmanagedType.SysUInt)]
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
        static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
    
        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
    
        public static string GetText(IntPtr hWnd)
        {
            int length = GetWindowTextLength(hWnd);
            StringBuilder sb = new StringBuilder(length + 1);
            GetWindowText(hWnd, sb, sb.Capacity);
            return sb.ToString();
        }
        private static string GetWindowClassName(IntPtr hWnd)
        {
            int nRet;
            StringBuilder ClassName = new StringBuilder(256);
            nRet = GetClassName(hWnd, ClassName, ClassName.Capacity);
            return (nRet != 0) ? ClassName.ToString() : null;
        }
        public static void SetText(IntPtr hWnd, String text)
        {
    
            UInt32 WM_SETTEXT = 0x000C;
            StringBuilder sb = new StringBuilder(text);
            int result = SendMessage(hWnd, WM_SETTEXT, (IntPtr)sb.Length, (String)sb.ToString());
        }

    您可以找到对话框的元素并在文件名文本框中输入文本,然后发送一个按钮单击以保存为按钮。

        private static bool EnumWindow(IntPtr handle, IntPtr pointer)
        {
            GCHandle gch = GCHandle.FromIntPtr(pointer);
            String window_class_name = GetWindowClassName(handle);
            // Set textbox text - filename to save
            if (string.Compare(window_class_name, "Edit", true, CultureInfo.InvariantCulture) == 0 ) {
                // http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx
                const UInt32 WM_CHAR = 0x0102;
                const UInt32 WM_KEYDOWN = 0x0100;
                const UInt32 WM_KEYUP = 0x0101;
                const UInt32 VK_RETURN = 0x0D;
                SendMessage(handle, WM_CHAR, new IntPtr(WM_KEYDOWN), IntPtr.Zero);
                SetText(handle, @"c:\temp\my random filename");
                Thread.Sleep(1000);
                SendMessage(handle, WM_CHAR, new IntPtr(VK_RETURN), IntPtr.Zero);
            }
            // Click 'Save'
            if (string.Compare(window_class_name, "Button", true, CultureInfo.InvariantCulture) == 0 ) {
                string button_text = GetText(handle);
                if (string.Compare(button_text, "&Save", true, CultureInfo.InvariantCulture) == 0) {
                    SetText(handle, "About to click");
                    const UInt32 BM_CLICK = 0x00F5;
                    Thread.Sleep(1000);
                    SendMessage(handle, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
                }
            }
    
            List<IntPtr> list = gch.Target as List<IntPtr>;
            if (list == null)
                throw new InvalidCastException("cast exception");
            list.Add(handle);
            return true;
        }

    Save As manipulation

    请注意,如果不发送“Enter”键,Windows Explorer 将忽略后台输入的文本,并以原始位置/名称保存文件。

    Save As manipulation

    修改后的代码包含在归档文件中。只需进行最少的努力,即可将类集成到 PowerShell 中,但要使示例真正有用,需要更多工作,并且超出了本文档的范围。

    另一个有趣的可能场景是,当目标网站托管在 Linux 主机上的 Tomcat 上运行时,但需要运行 Internet Explorer 集成测试。使用以下样板 Perl 代码片段,可以通过 ssh:cygwin、TeamCity、Jenkins 等远程启动 PowerShell 脚本:

    use Net::SSH::Perl;
    use Data::Dumper;
    use constant DEBUG => 0;
    
    our ($HOSTNAME, $USER, $PASSWORD );
    my $POWERSHELL_SCRIPT = ...
    $HOSTNAME = '192.168.56.102';
    $USER = 'cyg_server';
    $PASSWORD = 'cyg_server';
    
    # workaround cygwin console IO challenge
    my $ssh_command =
    "cat /dev/null|\
    /cygdrive/c/Windows/system32/WindowsPowerShell/v1.0/powershell.exe \
    -ExecutionPolicy Unrestricted -command \"&{ $POWERSHELL_SCRIPT }\"";
    
    print STDERR $ssh_command if (DEBUG) ;
    
    my $ssh = Net::SSH::Perl->new( $HOSTNAME, debug => 0 );
    $ssh->login( $USER, $PASSWORD );
    
    my ( $stdout, $stderr, $exitcode ) = $ssh->cmd( $ssh_command, undef );
    print STDERR Dumper \[ $stdout, $stderr, $exitcode ];
    
    1;
    
    END

    这显然不是 Selenium grid 测试脚本所必需的,但可以用于其他情况。

    例如,通过运行以下教科书式的 PowerShell 脚本通过 ssh:

    Import-module WebAdministration
    $WebSiteAlias = 'Test'
    $AppPoolAlias = 'Test'
    pushd 'IIS:\Sites\Default Web Site'
    $IISPath = "..\$WebSiteAlias"
    if (Test-Path $IISPath) {
      Write-Host "Web Site '$WebSiteAlias' exists."
    }
    $IISPath = "IIS:\AppPools"
    cd $IISPath
    if (Test-Path ".$AppPoolAlias") {
      Write-Host "Application Pool '$AppPoolAlias' exists."
    }

    结果将可供调用脚本使用……

    Linux invoking Powershell

    当业务运行混合 Tomcat / IIS 网站,并且由于某种原因必须从 Linux 机器编排部署时,这非常有用。在这种情况下,将使用更复杂的 PowerShell 代码,例如执行一些应用程序池检查,调用 msdeploy.exe,然后从 Linux 进行业务特定的网站“预热”。

    通用 Selenium 自动化

    以下 Selenium 自动化脚本片段从某个邮轮供应商网站中选择加勒比蜜月假期邮轮。选择目的地、日期范围和旅行人数的代码非常冗余,仅显示部分内容。完整的可运行脚本可在 zip 文件中找到。

    # Select destination
    $value1 = 'dest'
    $css_selector1 = ('a[data-param={0}]' -f $value1)
    try {
    
      [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
      $wait.PollingInterval = 150
      [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector1)))
      [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
    } catch [exception]{
      Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
    }
    
    $element1 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
    [NUnit.Framework.Assert]::IsTrue(($element1.Text -match 'Select a destination' ))
    
    Write-Output ('Clicking on ' + $element1.Text)
    $element1.Click()
    Start-Sleep 1
    
    $value2 =  'C'
    $css_selector2 = ('a[data-id={0}]' -f $value2)
    try {
      [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
      $wait.PollingInterval = 150
      [OpenQA.Selenium.Remote.RemoteWebElement]$element2 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector2)))
      [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))
    
    } catch [exception]{
      Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
    }
    $element2 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))
    
    Write-Output ('Clicking on ' + $element2.Text)
    [OpenQA.Selenium.Interactions.Actions]$actions2 = New-Object OpenQA.Selenium.Interactions.Actions ($selenium)
    $actions2.MoveToElement([OpenQA.Selenium.IWebElement]$element2).Build().Perform()
    $actions2.Click().Build().Perform()
    Start-Sleep 3
    
    
    $value1 = 'dat'
    $css_selector1 = ('a[data-param={0}]' -f $value1)
    try {
    
      [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
      $wait.PollingInterval = 150
      [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector1)))
    ##
      [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
    } catch [exception]{
      Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
    }
    
    $element1 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
    [NUnit.Framework.Assert]::IsTrue(($element1.Text -match 'Select a date'))
    
    Write-Output ('Clicking on ' + $element1.Text)
    $element1.Click()
    Start-Sleep 1
    
    $value2 =  '"022015"'
    $css_selector2 = ('a[data-id={0}]' -f $value2)
    try {
      [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
      $wait.PollingInterval = 150
      [OpenQA.Selenium.Remote.RemoteWebElement]$element2 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector2)))
    ##
      [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))
    
    } catch [exception]{
      Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
    }
    $element2 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))
    Write-Output ('Clicking on ' + $element2.Text)
    [OpenQA.Selenium.Interactions.Actions]$actions2 = New-Object OpenQA.Selenium.Interactions.Actions ($selenium)
    $actions2.MoveToElement([OpenQA.Selenium.IWebElement]$element2).Build().Perform()
    $actions2.Click().Build().Perform()
    Start-Sleep 3
    
    $value1 = 'numGuests'
    $css_selector1 = ('a[data-param={0}]' -f $value1)
    try {
      [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
      $wait.PollingInterval = 150
      [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector1)))
    ##
      [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
    } catch [exception]{
      Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
    }
    
    $element1 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
    [NUnit.Framework.Assert]::IsTrue(($element1.Text -match 'How many travelers'))
    
    Write-Output ('Clicking on ' + $element1.Text)
    $element1.Click()
    Start-Sleep 1
    
    $value2 =  '"2"'
    $css_selector2 = ('a[data-id={0}]' -f $value2)
    try {
      [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
      $wait.PollingInterval = 150
    
      [OpenQA.Selenium.Remote.RemoteWebElement]$element2 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector2)))
    ##
      [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))
    
    } catch [exception]{
      Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
    }
    $element2 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector2))
    Write-Output ('Clicking on ' + $element2.Text)
    [OpenQA.Selenium.Interactions.Actions]$actions2 = New-Object OpenQA.Selenium.Interactions.Actions ($selenium)
    $actions2.MoveToElement([OpenQA.Selenium.IWebElement]$element2).Build().Perform()
    $actions2.Click().Build().Perform()
    Start-Sleep 3
    
    $css_selector1 = 'div.actions > a.search'
    try {
      [void]$selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
    } catch [exception]{
      Write-Output ("Exception : {0} ...`n" -f (($_.Exception.Message) -split "`n")[0])
    }
    
    $element1 = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector1))
    [NUnit.Framework.Assert]::IsTrue(($element1.Text -match 'SEARCH'))
    Write-Output ('Clicking on ' + $element1.Text)
    $element1.Click()
    
    Start-Sleep 10
    
    try {
      [OpenQA.Selenium.Screenshot]$screenshot =  $selenium.GetScreenshot()
      $guid = [guid]::NewGuid()
      $image_name = ($guid.ToString())
      [string]$image_path = ('{0}\{1}\{2}.{3}' -f (Get-ScriptDirectory),'temp',$image_name,'.jpg')
      $screenshot.SaveAsFile($image_path,[System.Drawing.Imaging.ImageFormat]::Jpeg)
    
    } catch [exception]{
      Write-Output $_.Exception.Message
    }
    # Cleanup
    try {
      $selenium.Quit()
    } catch [exception]{
      # Ignore errors if unable to close the browser
    }

    Selenium

    该脚本可以在除 IE 11 以外的任何浏览器中成功重放。以下代码选择浏览器。

    param(
      [string]$browser,
      [int]$version
    )
    ...
    if ($browser -ne $null -and $browser -ne '') {
      try {
        $connection = (New-Object Net.Sockets.TcpClient)
        $connection.Connect("127.0.0.1",4444)
        $connection.Close()
      } catch {
        Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\hub.cmd"
        Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\node.cmd"
        Start-Sleep -Seconds 10
      }
      Write-Host "Running on ${browser}"
      if ($browser -match 'firefox') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Firefox()
    
      }
      elseif ($browser -match 'chrome') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Chrome()
      }
      elseif ($browser -match 'ie') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::InternetExplorer()
        if ($version -ne $null -and $version -ne 0) {
          $capability.SetCapability("version", $version.ToString());
        }
      }
    
      elseif ($browser -match 'safari') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Safari()
      }
      else {
        throw "unknown browser choice:${browser}"
      }
      $uri = [System.Uri]("http://127.0.0.1:4444/wd/hub")
      $selenium = New-Object OpenQA.Selenium.Remote.RemoteWebDriver ($uri,$capability)
    } else {
      Write-Host 'Running on phantomjs'
    ...

    执行时,脚本会打印最少的面包屑,指示已执行的操作。

    使用 Selenium sendKeys 上传文件

    以下示例在 www.freetranslation.com 上翻译文本。页面包含以下片段。

    Three button prompt

    <div class="gw-upload-action clearfix">
      <div id="upload-button" class="btn"><img class="gw-icon upload" alt="" src="http://d2yxcfsf8zdogl.cloudfront.net/home-php/assets/home/img/pixel.gif"/>
             Choose File(s)                        
            <div class="ajaxupload-wrapper" style="width: 300px; height: 50px;"><input class="ajaxupload-input" type="file" name="file" multiple=""/></div>
        </div>
    </div>

    脚本将文本写入文件并上传。

    [void]$selenium.Manage().timeouts().ImplicitlyWait([System.TimeSpan]::FromSeconds(60))
    
    $base_url = 'http://www.freetranslation.com/'
    
    $text_file = ('{0}\{1}' -f (Get-ScriptDirectory),'testfile.txt')
    Write-Output 'good morning driver' | Out-File -FilePath $text_file -Encoding ascii
    $selenium.Navigate().GoToUrl($base_url)
    $selenium.Manage().Window.Maximize()
    $upload_element = $selenium.FindElement([OpenQA.Selenium.By]::ClassName('ajaxupload-input'))
    $upload_element.SendKeys($text_file)

    然后等待以下元素出现。

    Three button prompt

    <a href="..." class="gw-download-link">
      <img class="gw-icon download" src="http://d2yxcfsf8zdogl.cloudfront.net/home-php/assets/home/img/pixel.gif"/>
      Download
    </a>
    [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(3))
    $wait.PollingInterval = 100
    
    [OpenQA.Selenium.Remote.RemoteWebElement]$element1 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::ClassName("gw-download-link")))
    
    [OpenQA.Selenium.Remote.RemoteWebElement]$element2 = $wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector('img.gw-icon')))
    $text_url = $element1.getAttribute('href')

    并下载结果。

    $result = Invoke-WebRequest -Uri $text_url
    [NUnit.Framework.Assert]::IsTrue(($result.RawContent -match 'Bonjour pilote'))

    并与已知翻译进行验证。

    Selenium IDE PowerShell 格式化程序

    接下来,您将从管道中排除 C#,并在 Selenium IDE 中直接录制 PowerShell 事务。完全支持自定义格式化;您无需在早期开发阶段打包 xpi

    要继续,作者会 fork 其中一个现有存储库,由 David Zwarg 提供,并修改 C# 格式化程序以遵循 PowerShell 语法并进行其他必要的调整。创建格式化程序所需的所有内容只有一个文件。

    Three button prompt

    需要小心的一点是不要从基于Selenium Remote Control 的插件开始:RC 插件可以开发,但协议已过时,特别是没有无头驱动程序可用。

    格式化程序的完整 JavaScript 源代码尚未在此处显示:这是 alpha 质量的设计,并且拉取请求正在等待中。在 IDE 命令、中间 JavaScript 方法原型和最终 C# 方法调用之间进行转换相当痛苦。

    源代码可在作者的 github 存储库中找到。

    该插件继承自 webdriver.js

    if (!this.formatterType) {
      var subScriptLoader = Components.classes['@mozilla.org/moz/jssubscript-loader;1'].getService(Components.interfaces.mozIJSSubScriptLoader);
      subScriptLoader.loadSubScript('chrome://selenium-ide/content/formats/webdriver.js', this);
    }

    目前它只添加了最少的功能——目前有相当多的格式化程序具有几乎相同的代码。

    修改包括在所有方法引用中提供完整的类路径,例如:

    WDAPI.Utils.isElementPresent = function(how, what) {
    return "IsElementPresent(" + WDAPI.Driver.searchContext(how, what) + ")";
    };

    变成

    WDAPI.Utils.isElementPresent = function(how, what) {
      return '[Selenium.Internal.SeleniumEmulation]::IsElementPresent(' + WDAPI.Driver.searchContext(how, what) + ')';
    };

    并调整语义,例如:

    Equals.prototype.toString = function() {
      return this.e1.toString() + ' == ' + this.e2.toString() ;
    }

    变成

    Equals.prototype.toString = function() {
      return this.e1.toString() + ' -eq ' + this.e2.toString();
    };

    使用 Nunit.dll 看起来很自然,但是访问 StringAssert 似乎有点问题,因此您可以选择使用 Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll,如前所述。

    早期示例中的所有 PowerShell 初始化代码都进入驱动程序类的头部选项。

    this.options = {
      receiver: '$selenium',
      base_url: 'http://docs.seleniumhq.org/docs/02_selenium_ide.jsp',
      driver_namespace: "OpenQA.Selenium.Firefox",
      driver_capabilities: "Firefox()",
      showSelenese: 'false',
      indent: '4',
      initialIndents:  '3',
      header:
         'Param (\n'+
         indents(1) + '[switch] $browser\n'+
         ')\n'
      // ...
       '$capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::${driver_capabilities}\n' +
      // ...
    
      footer:
         '# Cleanup\n' +
         'try {\n' +
         indents(1) + '$selenium.Quit()\n' +
         '} catch [Exception] {\n' +
         indents(1) + '# Ignore errors if unable to close the browser\n' +
              '}\n',
      defaultExtension: 'ps1'
    };

    关键属性转换为常规格式化程序输入。

    this.configForm =
        '<description>Selenium instance name</description>' +
        '<textbox id="options_receiver" />' +
        '<description>WebDriver Capabilities</description>' +
        '<menulist id="options_driver_capabilities"><menupopup>' +
        '<menuitem label="Firefox" value="Firefox()"/>' +
        '<menuitem label="Google Chrome" value="Chrome()"/>' +
        '<menuitem label="Safari" value="Safari()"/>' +
        '<menuitem label="Internet Explorer" value="InternetExplorer()"/>' +
        '</menupopup></menulist>'+
      // ...

    Selenium IDE options

    在开发后期,您将安排源代码以适合 xpi,并创建 chrome.manifestinstall.rdfformat-loader.xul,例如:

    <?xml version="1.0"?>
    <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
    
    <overlay id="webdriver_format_loader_overlay"
             xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
             xmlns:html="http://www.w3.org/1999/xhtml">
        <script type="application/x-javascript" src="chrome://selenium-ide/content/api.js"/>
        <html:script type="application/javascript">
            var ide_api = new API();
            ide_api.addPlugin("powershell-webdriver-formatter@serguei.kouzmine");
            ide_api.addPluginProvidedFormatter("powershell-webdriver", "Powershell - WebDriver", "chrome://powershell-webdriver-formatter/content/formats/powershell-webdriver.js");
            ide_api.addPluginProvidedFormatter("powershell-remotecontrol", "Powershell - RC", "chrome://powershell-webdriver-formatter/content/formats/powershell-remotecontrol.js");
        </html:script>
    </overlay>

    这可以通过简单的批处理命令(或等效的 bash 脚本)打包成独立的 Firefox 附加组件。

    @echo off
    setlocal
    
    pushd %~dp0
    
    
    set APP_NAME="powershell-webdriver-formatter"
    set CHROME_PROVIDERS="content"
    
    set ROOT_DIR=%CD%
    set TMP_DIR="build"
    
    REM remove any left-over files from previous build
    del /Q %APP_NAME%.xpi
    del /S /Q %TMP_DIR%
    
    mkdir %TMP_DIR%\chrome\content
    
    robocopy.exe content %TMP_DIR%\chrome\content /E
    robocopy.exe locale %TMP_DIR%\chrome\locale /E
    robocopy.exe skin %TMP_DIR%\chrome\skin /E
    robocopy.exe defaults %TMP_DIR%\defaults /E
    copy install.rdf %TMP_DIR%
    copy chrome.manifest.production %TMP_DIR%\chrome.manifest
    
    rem Package the XPI file
    cd %TMP_DIR%
    echo "Generating %APP_NAME%.xpi..."
    
    PATH=%PATH%;%ProgramFiles%\7-Zip;%ProgramFiles(x86)%\7-Zip
    
    7z.exe a -r -y -tzip ../%APP_NAME%.zip *
    
    cd %ROOT_DIR%
    rename %APP_NAME%.zip %APP_NAME%.xpi
    
    endlocal

    Thre logo border

    要使用格式化程序:

    • 打开 Selenium IDE,录制事务。
    • 从 Options 菜单中选择 Options。
    • 选择“Formats”选项卡。
      • 如果加载了格式化程序 xpi,请填写输入框,或者
      • 单击“Add”按钮。
      • 命名格式。
      • 粘贴并保存 JavaScript 源(丢失输入)。
    • 在“File” -> “Export Test Case As...”中,选择格式。

    如果一切顺利,生成的 PowerShell 脚本将无需修改即可立即运行。

    例如,在以下片段中,在加载所需的程序集并启动 Selenium 后,通过在加载的页面上下文中执行 JavaScript 代码,在 Google Logo 周围绘制边框。

    $selenium.Navigate().GoToUrl('http://www.google.com')
    [OpenQA.Selenium.IWebElement] $element = $selenium.FindElement([OpenQA.Selenium.By]::Id("hplogo"))[OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteScript("arguments[0].setAttribute('style', arguments[1]);", $element, "color: yellow; border: 4px solid yellow;")
    start-sleep 3
      [OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteScript("arguments[0].setAttribute('style', arguments[1]);", $element, '')

    The Logo border

    显然,这里的 JavaScript 是唯一重要的部分。牺牲 C# 项目的开销似乎是合适的。

    另一个可能的示例将执行 $selenium.Manage().Timeouts().setScriptTimeout[OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteAsyncScript,然后是 $selenium.FindElement,以便在页面上“盖上”构建信息,或者执行检查并将答案存储在动态追加的 div 元素中,并将断言结果传回脚本(进行中)。

    小型开发活动,例如标准的 CI 部署后网站“预热”,也可能通过 Selenium IDE 结合 PowerShell 启动更容易,而不是通过编写单独的应用程序。

    在 Explorer 任务栏上显示 Selenium 调试消息

    以下示例结合了 Hosting And Changing Controls In Other Applications 的代码和一个典型的 Selenium 事务(此事务涉及框架)。一些网站确实对鼠标悬停事件进行了编码。本例在没有附加监视器(例如在 VirtualBox 中)的情况下,浏览器最大化填满屏幕,没有空间来跟踪执行,因此展示了在此情况下调试事务。

    来自 Hosting And Changing Controls In Other Applications 的负责向正在运行的窗口添加额外控件的代码在不进行修改的情况下使用,但计划进行一些更改,将源代码与脚本一起保留,而不是编译成程序集。

    Add-Type -TypeDefinition @"
    namespace System.Windows
    {
        class Win32WindowEvents
        {
    //...
        public static class WinAPI
        {
    //...
            public static class Win32ControlType
            {
                public static string Button = "Button";
    
    //... 
        ///

    目标是将 Windows 控件固定到任务栏。

    function custom_debug {
      param(
        [System.Management.Automation.PSReference]$local:button_ref,
        [string]$message
      )
      Write-Debug $message
      $local:button = $local:button_ref.Value
      if ($local:button -eq $null) {
        $exlorer_window = [System.Windows.Win32Window]::FromProcessName('explorer')
        # $window.ClassName = Shell_TrayWnd
        $exlorer_window.Title = "A control WINDOW";
        $local:button = New-Object System.Windows.Win32Button
        # NOTE: The position and size are manually set
        $local:button.TopMost = $true
        $local:button.Width = 600
        $local:button.Height = 60
        $x = ($exlorer_window.Position.Right - $local:button.Width)
        $y = -20
    
        $local:button.Pos_X = $x
        $local:button.Pos_Y = $y
    
        $local:button.Font = New-Object System.Drawing.Font ('Microsoft Sans Serif',7,[System.Drawing.FontStyle]::Regular,[System.Drawing.GraphicsUnit]::Point,0)
        $exlorer_window.AddControl($local:button)
    
        $local:button_ref.Value = $local:button
      }
      $local:button.Text = $message
    }

    The Debug button

    此按钮用于显示调试消息和(进行中)暂停脚本的执行。

    $shared_assemblies = @(
      'WebDriver.dll',
      'WebDriver.Support.dll',
      'nunit.core.dll',
      'nunit.framework.dll'
    )
    
    
    $shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'
    
    if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
      $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
    }
    
    pushd $shared_assemblies_path
    $shared_assemblies | ForEach-Object {
    
      if ($host.Version.Major -gt 2) {
        Unblock-File -Path $_;
      }
      Write-Debug $_
      Add-Type -Path $_
    }
    popd
    [void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
    
    $DebugPreference = 'Continue'
    # Convertfrom-JSON applies To: Windows PowerShell 3.0 and above
    [NUnit.Framework.Assert]::IsTrue($host.Version.Major -gt 2)
    
    $hub_host = '127.0.0.1'
    $hub_port = '4444'
    
    $uri = [System.Uri](('http://{0}:{1}/wd/hub' -f $hub_host,$hub_port))
    [object]$button = $null
    
    custom_debug ([ref]$button) 'Starting firefox'
    
    if ($browser -ne $null -and $browser -ne '') {
      try {
        $connection = (New-Object Net.Sockets.TcpClient)
        $connection.Connect($hub_host,[int]$hub_port)
        $connection.Close()
      } catch {
        Start-Process -FilePath 'C:\Windows\System32\cmd.exe' -ArgumentList 'start cmd.exe /c c:\java\selenium\hub.cmd'
        Start-Process -FilePath 'C:\Windows\System32\cmd.exe' -ArgumentList 'start cmd.exe /c c:\java\selenium\node.cmd'
        Start-Sleep -Seconds 10
      }
      Write-Host "Running on ${browser}"
      if ($browser -match 'firefox') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Firefox()
    
      }
      elseif ($browser -match 'chrome') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Chrome()
      }
      elseif ($browser -match 'ie') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::InternetExplorer()
      }
      elseif ($browser -match 'safari') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Safari()
      }
      else {
        throw "unknown browser choice:${browser}"
      }
      $selenium = New-Object OpenQA.Selenium.Remote.RemoteWebDriver ($uri,$capability)
    } else {
      # this example may not work with phantomjs 
      $phantomjs_executable_folder = "c:\tools\phantomjs"
      Write-Host 'Running on phantomjs'
      $selenium = New-Object OpenQA.Selenium.PhantomJS.PhantomJSDriver ($phantomjs_executable_folder)
      $selenium.Capabilities.SetCapability("ssl-protocol","any")
      $selenium.Capabilities.SetCapability("ignore-ssl-errors",$true)
      $selenium.Capabilities.SetCapability("takesScreenshot",$true)
      $selenium.Capabilities.SetCapability("userAgent","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.7 Safari/534.34")
      $options = New-Object OpenQA.Selenium.PhantomJS.PhantomJSOptions
      $options.AddAdditionalCapability("phantomjs.executable.path",$phantomjs_executable_folder)
    }
    
    [void]$selenium.Manage().timeouts().ImplicitlyWait([System.TimeSpan]::FromSeconds(60))
    
    $selenium.url = $base_url = 'http://translation2.paralink.com'
    $selenium.Navigate().GoToUrl(($base_url + '/'))
    
    [string]$xpath = "//frame[@id='topfr']"
    [object]$top_frame = $null
    find_page_element_by_xpath ([ref]$selenium) ([ref]$top_frame) $xpath
    $current_frame = $selenium.SwitchTo().Frame($top_frame)
    
    [NUnit.Framework.Assert]::AreEqual($current_frame.url,('{0}/{1}' -f $base_url,'newtop.asp'),$current_frame.url)
    Write-Debug ('Switched to {0} {1}' -f $current_frame.url,$xpath)
    custom_debug ([ref]$button) ('Switched to {0} {1}' -f $current_frame.url,$xpath)
    $top_frame = $null
    
    [string]$text = 'Spanish-Russian translation'
    $css_selector = 'select#directions > option[value="es/ru"]'
    [OpenQA.Selenium.IWebElement]$element = $null
    find_page_element_by_css_selector ([ref]$current_frame) ([ref]$element) $css_selector
    [NUnit.Framework.Assert]::AreEqual($text,$element.Text,$element.Text)
    custom_debug ([ref]$button) ('selected "{0}"' -f $text)
    $element.Click()
    $element = $null
    
    custom_pause
    
    [string]$xpath2 = "//textarea[@id='source']"
    
    [OpenQA.Selenium.IWebElement]$element = $null
    find_page_element_by_xpath ([ref]$current_frame) ([ref]$element) $xpath2
    highlight ([ref]$current_frame) ([ref]$element)
    [OpenQA.Selenium.Interactions.Actions]$actions = New-Object OpenQA.Selenium.Interactions.Actions ($current_frame)
    $actions.MoveToElement([OpenQA.Selenium.IWebElement]$element).Click().Build().Perform()
    
    $text = @"
    Yo, Juan Gallo de Andrada, escribano de C?mara del Rey nuestro se?or, de los que residen en su Consejo, certifico y doy fe que, habiendo visto por los se?ores d?l un libro intitulado El ingenioso hidalgo de la Mancha, compuesto por Miguel de Cervantes Saavedra, tasaron cada pliego del dicho libro a tres maraved?s y medio; el cual tiene ochenta y tres pliegos, que al dicho precio monta el dicho libro docientos y noventa maraved?s y medio, en que se ha de vender en papel;.
    "@
    [void]$element.SendKeys($text)
    custom_debug ([ref]$button) ('Entered "{0}"' -f $text.Substring(0,100))
    $element = $null
    
    Start-Sleep -Milliseconds 1000
    $css_selector = 'img[src*="btn-en-tran.gif"]'
    
    $title = 'Translate'
    find_page_element_by_css_selector ([ref]$current_frame) ([ref]$element) $css_selector
    [NUnit.Framework.Assert]::AreEqual($title,$element.GetAttribute('title'),$element.GetAttribute('title'))
    highlight ([ref]$current_frame) ([ref]$element)
    [OpenQA.Selenium.Interactions.Actions]$actions = New-Object OpenQA.Selenium.Interactions.Actions ($current_frame)
    $actions.MoveToElement([OpenQA.Selenium.IWebElement]$element).Click().Build().Perform()
    custom_debug ([ref]$button) ('Clicked on "{0}"' -f $title)
    
    $element = $null
    custom_pause
    
    [void]$selenium.SwitchTo().DefaultContent()
    
    [string]$xpath = "//frame[@id='botfr']"
    [object]$bot_frame = $null
    find_page_element_by_xpath ([ref]$selenium) ([ref]$bot_frame) $xpath
    $current_frame = $selenium.SwitchTo().Frame($bot_frame)
    [NUnit.Framework.Assert]::AreEqual($current_frame.url,('{0}/{1}' -f $base_url,'newbot.asp'),$current_frame.url)
    custom_debug ([ref]$button) ('Switched to {0}' -f $current_frame.url)
    $bot_frame = $null
    
    [string]$xpath2 = "//textarea[@id='target']"
    
    [OpenQA.Selenium.IWebElement]$element = $null
    find_page_element_by_xpath ([ref]$current_frame) ([ref]$element) $xpath2
    highlight ([ref]$current_frame) ([ref]$element)
    $text = $element.Text
    custom_debug ([ref]$button) ('Read "{0}"' -f $text.Substring(0,100))
    custom_pause
    
    # https://code.google.com/p/selenium/source/browse/java/client/src/org/openqa/selenium/remote/HttpCommandExecutor.java?r=3f4622ced689d2670851b74dac0c556bcae2d0fe
    # write-output $frame.PageSource
    [void]$selenium.SwitchTo().DefaultContent()
    
    $current_frame = $selenium.SwitchTo().Frame(1)
    [NUnit.Framework.Assert]::AreEqual($current_frame.url,('{0}/{1}' -f $base_url,'newbot.asp'),$current_frame.url)
    
    custom_pause
    
    [void]$selenium.SwitchTo().DefaultContent()
    $current_frame = $selenium.SwitchTo().Frame(0)
    [NUnit.Framework.Assert]::AreEqual($current_frame.url,('{0}/{1}' -f $base_url,'newtop.asp'),$current_frame.url)
    custom_debug ([ref]$button) ('Switched to {0}' -f $current_frame.url)
    custom_pause
    
    [void]$selenium.SwitchTo().DefaultContent()
    Write-Debug ('Switched to {0}' -f $selenium.url)
    
    # Cleanup
    cleanup ([ref]$selenium)
    
    $button.Visible = $false

    The Transaction

    完整源代码可在 zip 文件中找到。

    Selenium EventFiring WebDriver 示例

    以下是 SeleniumEventFiringWebDriver 从 PowerShell 访问的快速示例。通过在 Selenium 事件之后运行代码,可以捕获 Ajax 自动建议的结果。

    param(
      [string]$browser = 'firefox',
      [int]$event_delay = 250,
      [switch]$pause
    
    )
    
    
    function netstat_check
    {
      param(
        [string]$selenium_http_port = 4444
      )
    
      $results = Invoke-Expression -Command "netsh interface ipv4 show tcpconnections"
    
      $t = $results -split "`r`n" | Where-Object { ($_ -match "\s$selenium_http_port\s") }
      (($t -ne '') -and $t -ne $null)
    
    }
    
    function cleanup
    {
      param(
        [System.Management.Automation.PSReference]$selenium_ref
      )
      try {
        $selenium_ref.Value.Quit()
      } catch [exception]{
        Write-Output (($_.Exception.Message) -split "`n")[0]
        # Ignore errors if unable to close the browser
      }
    }
    
    $shared_assemblies = @(
      'WebDriver.dll',
      'WebDriver.Support.dll',# for Events
      'nunit.core.dll',
      'nunit.framework.dll'
    )
    
    $shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'
    
    if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
      $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
    }
    pushd $shared_assemblies_path
    $shared_assemblies | ForEach-Object {
      # Unblock-File -Path $_; 
      Add-Type -Path $_
    }
    popd
    
    [void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
    $verificationErrors = New-Object System.Text.StringBuilder
    $phantomjs_executable_folder = "C:\tools\phantomjs"
    if ($browser -ne $null -and $browser -ne '') {
      try {
        $connection = (New-Object Net.Sockets.TcpClient)
        $connection.Connect("127.0.0.1",4444)
        $connection.Close()
      } catch {
        Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\hub.cmd"
        Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\node.cmd"
        Start-Sleep -Seconds 10
      }
      Write-Host "Running on ${browser}" -foreground 'Yellow'
      if ($browser -match 'firefox') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Firefox()
    
      }
      elseif ($browser -match 'chrome') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Chrome()
      }
      elseif ($browser -match 'ie') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::InternetExplorer()
        if ($version -ne $null -and $version -ne 0) {
          $capability.SetCapability("version",$version.ToString());
        }
    
      }
      elseif ($browser -match 'safari') {
        $capability = [OpenQA.Selenium.Remote.DesiredCapabilities]::Safari()
      }
      else {
        throw "unknown browser choice:${browser}"
      }
      $uri = [System.Uri]("http://127.0.0.1:4444/wd/hub")
      $selenium = New-Object OpenQA.Selenium.Remote.RemoteWebDriver ($uri,$capability)
    } else {
      Write-Host 'Running on phantomjs' -foreground 'Yellow'
      $phantomjs_executable_folder = "C:\tools\phantomjs"
      $selenium = New-Object OpenQA.Selenium.PhantomJS.PhantomJSDriver ($phantomjs_executable_folder)
      $selenium.Capabilities.SetCapability("ssl-protocol","any")
      $selenium.Capabilities.SetCapability("ignore-ssl-errors",$true)
      $selenium.Capabilities.SetCapability("takesScreenshot",$true)
      $selenium.Capabilities.SetCapability("userAgent","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.7 Safari/534.34")
      $options = New-Object OpenQA.Selenium.PhantomJS.PhantomJSOptions
      $options.AddAdditionalCapability("phantomjs.executable.path",$phantomjs_executable_folder)
    }
    
    
    if ($host.Version.Major -le 2) {
      [void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
      $selenium.Manage().Window.Size = New-Object System.Drawing.Size (600,400)
      $selenium.Manage().Window.Position = New-Object System.Drawing.Point (0,0)
    } else {
      $selenium.Manage().Window.Size = @{ 'Height' = 400; 'Width' = 600; }
      $selenium.Manage().Window.Position = @{ 'X' = 0; 'Y' = 0 }
    }
    
    $window_position = $selenium.Manage().Window.Position
    $window_size = $selenium.Manage().Window.Size
    
    $base_url = 'http://www.google.com/'
    
    # TODO: invoke NLog assembly for quicker logging triggered by the events
    # www.codeproject.com/Tips/749612/How-to-NLog-with-VisualStudio
    
    $event = New-Object -Type 'OpenQA.Selenium.Support.Events.EventFiringWebDriver' -ArgumentList @( $selenium)
    
    $element_value_changing_handler = $event.add_ElementValueChanging
    $element_value_changing_handler.Invoke(
      {
        param(
          [object]$sender,
          [OpenQA.Selenium.Support.Events.WebElementEventArgs]$eventargs
        )
        Write-Host 'Value Change handler' -foreground 'Yellow'
        if ($eventargs.Element.GetAttribute('id') -eq 'gbqfq') {
          $xpath1 = "//div[@class='sbsb_a']"
          try {
            [OpenQA.Selenium.IWebElement]$local:element = $sender.FindElement([OpenQA.Selenium.By]::XPath($xpath1))
          } catch [exception]{
          }
          Write-Host $local:element.Text -foreground 'Blue'
        }
    
      })
    
    $verificationErrors = New-Object System.Text.StringBuilder
    $base_url = 'http://www.google.com'
    $event.Navigate().GoToUrl($base_url)
    
    # protect from blank page
    [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($event,[System.TimeSpan]::FromSeconds(10))
    $wait.PollingInterval = 50
    [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::Id("hplogo")))
    
    $xpath = "//input[@id='gbqfq']"
    
    # for mobile
    # $xpath = "//input[@id='mib']"
    
    [OpenQA.Selenium.IWebElement]$element = $event.FindElement([OpenQA.Selenium.By]::XPath($xpath))
    
    # http://software-testing-tutorials-automation.blogspot.com/2014/05/how-to-handle-ajax-auto-suggest-drop.html
    $element.SendKeys('Sele')
    # NOTE:cannot use 
    # [OpenQA.Selenium.Interactions.Actions]$actions = New-Object OpenQA.Selenium.Interactions.Actions ($event)
    # $actions.SendKeys($element,'Sele')
    Start-Sleep -Millisecond $event_delay
    $element.SendKeys('nium')
    Start-Sleep -Millisecond $event_delay
    $element.SendKeys(' webdriver')
    Start-Sleep -Millisecond $event_delay
    $element.SendKeys(' C#')
    Start-Sleep -Millisecond $event_delay
    $element.SendKeys(' tutorial')
    Start-Sleep -Millisecond $event_delay
    $element.SendKeys([OpenQA.Selenium.Keys]::Enter)
    Start-Sleep 10
    
    # Cleanup
    cleanup ([ref]$event)

    The Google suggestions

    杂项。实用程序

    您可以将 C# 的控制台监视器移植到 PowerShell,以在需要时由某些持续集成构建自动化在网格计算机上定期收集桌面屏幕截图。

    # https://codeproject.org.cn/Tips/816113/Console-Monitor
    Add-Type -TypeDefinition @"
    // "
    using System;
    using System.Drawing;
    using System.IO;
    using System.Windows.Forms;
    using System.Drawing.Imaging;
    public class WindowHelper
    {
        private int _count = 0;
        public int Count
        {
            get { return _count; }
            set { _count = value; }
        }
       public String TakeScreenshot()
        {
            Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
            Graphics gr = Graphics.FromImage(bmp);
            gr.CopyFromScreen(0, 0, 0, 0, bmp.Size);
            string str = string.Format(@"C:\temp\Snap[{0}].jpeg", _count);
            bmp.Save(str, ImageFormat.Jpeg);
            bmp.Dispose();
            gr.Dispose();
            return str;
        }
        public WindowHelper()
        {
        }
    
    }
    
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll','System.Drawing.dll','System.Data.dll'
    $timer = New-Object System.Timers.Timer
    
    [int32]$max_iterations = 20
    [int32]$iteration = 0
    
    $action = {
    
      Write-Host "Iteration # ${iteration}"
      Write-Host "Timer Elapse Event: $(get-date -Format 'HH:mm:ss')"
      $owner = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
      $owner.count = $iteration
      $owner.Screenshot()
      $iteration++
      if ($iteration -ge $max_iterations)
      {
        Write-Host 'Stopping'
        $timer.stop()
        Unregister-Event thetimer -Force 
        Write-Host 'Completed'   
      }
    }
    
    Register-ObjectEvent -InputObject $timer -EventName elapsed -SourceIdentifier thetimer -Action $action

    请注意,您无法将数据按引用传递给从计时器事件调用的脚本函数,因此您无法远程执行 Add-Type。

     $action = {
    
      param(
       [System.Management.Automation.PSReference] $ref_screen_grabber 
    )
      [Win32Window]$screen_grabber = $ref_screen_grabber.Value

    接着

    Register-ObjectEvent -InputObject $timer -EventName elapsed -SourceIdentifier thetimer -Action $action -MessageData  ([ref]$owner )

    将导致中断。进一步调试此问题正在进行中。

    要切换 PowerShell 控制台窗口在显示窗体时最小化,可以使用以下代码:

    Add-Type -Name Window -Namespace Console -MemberDefinition @"
    // "
    [DllImport("Kernel32.dll")]
    public static extern IntPtr GetConsoleWindow();
     
    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
    "@

    屏幕截图

    您可以将 C# 的控制台监视器移植到 PowerShell,以在需要时由某些持续集成构建自动化在网格计算机上定期收集桌面屏幕截图。

    # https://codeproject.org.cn/Tips/816113/Console-Monitor
    Add-Type -TypeDefinition @"
    // "
    using System;
    using System.Drawing;
    using System.IO;
    using System.Windows.Forms;
    using System.Drawing.Imaging;
    public class WindowHelper
    {
        private int _count = 0;
        public int Count
        {
            get { return _count; }
            set { _count = value; }
        }
       public String TakeScreenshot()
        {
            Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
            Graphics gr = Graphics.FromImage(bmp);
            gr.CopyFromScreen(0, 0, 0, 0, bmp.Size);
            string str = string.Format(@"C:\temp\Snap[{0}].jpeg", _count);
            bmp.Save(str, ImageFormat.Jpeg);
            bmp.Dispose();
            gr.Dispose();
            return str;
        }
        public WindowHelper()
        {
        }
    
    }
    
    "@ -ReferencedAssemblies 'System.Windows.Forms.dll','System.Drawing.dll','System.Data.dll'
    $timer = New-Object System.Timers.Timer
    
    [int32]$max_iterations = 20
    [int32]$iteration = 0
    
    $action = {
    
      Write-Host "Iteration # ${iteration}"
      Write-Host "Timer Elapse Event: $(get-date -Format 'HH:mm:ss')"
      $owner = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
      $owner.count = $iteration
      $owner.Screenshot()
      $iteration++
      if ($iteration -ge $max_iterations)
      {
        Write-Host 'Stopping'
        $timer.stop()
        Unregister-Event thetimer -Force 
        Write-Host 'Completed'   
      }
    }
    
    Register-ObjectEvent -InputObject $timer -EventName elapsed -SourceIdentifier thetimer -Action $action

    请注意,您无法将数据按引用传递给从计时器事件调用的脚本函数,因此您无法远程执行 Add-Type。

     $action = {
    
      param(
       [System.Management.Automation.PSReference] $ref_screen_grabber 
    )
      [Win32Window]$screen_grabber = $ref_screen_grabber.Value

    接着

    Register-ObjectEvent -InputObject $timer -EventName elapsed -SourceIdentifier thetimer -Action $action -MessageData  ([ref]$owner )

    将导致中断。进一步调试此问题正在进行中。

    [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
    $Form = New-Object System.Windows.Forms.Form
     
    $showButton = New-Object System.Windows.Forms.Button
    $showButton.Text = 'ShowConsole'
    $showButton.Top = 10
    $showButton.Left = 10
    $showButton.Width = 100
    $showButton.add_Click({Show-Console})
    $form.controls.Add($showButton)
     
    $hideButton = New-Object System.Windows.Forms.Button
    $hideButton.Text = 'HideConsole'
    $hideButton.Top = 60
    $hideButton.Left = 10
    $hideButton.Width = 100
    $hideButton.add_Click({hide-Console})
    $form.controls.Add($hideButton)
     
    $Form.ShowDialog()

    IpBox

    函数操作来自winuser.h 的常量。

    function Show-Console {
       $consolePtr = [Console.Window]::GetConsoleWindow()
      #5 show
     [Console.Window]::ShowWindow($consolePtr, 5)
    }
     
    function Hide-Console {
        $consolePtr = [Console.Window]::GetConsoleWindow()
      #0 hide
     [Console.Window]::ShowWindow($consolePtr, 0)
    }

    IpBox

    在 PowerShell ISE 中编写 Selenium 脚本<</a>

    您可能会发现使用 Poweshell ISE 结合 Firebug 或其他浏览器托管的开发人员工具来编写实际脚本很方便。

    param(
      [string]$hub_host = '127.0.0.1',
      [string]$browser,
      [string]$version,
      [string]$profile = 'Selenium',
      [switch]$pause = $true
    )
    
    function set_timeouts {
      param(
        [System.Management.Automation.PSReference]$selenium_ref,
        [int]$explicit = 120,
        [int]$page_load = 600,
        [int]$script = 3000
      )
    
      [void]($selenium_ref.Value.Manage().Timeouts().ImplicitlyWait([System.TimeSpan]::FromSeconds($explicit)))
      [void]($selenium_ref.Value.Manage().Timeouts().SetPageLoadTimeout([System.TimeSpan]::FromSeconds($pageload)))
      [void]($selenium_ref.Value.Manage().Timeouts().SetScriptTimeout([System.TimeSpan]::FromSeconds($script)))
    
    }
    
    # http://stackoverflow.com/questions/8343767/how-to-get-the-current-directory-of-the-cmdlet-being-executed
    function Get-ScriptDirectory
    {
      $Invocation = (Get-Variable MyInvocation -Scope 1).Value
      if ($Invocation.PSScriptRoot) {
        $Invocation.PSScriptRoot
      }
      elseif ($Invocation.MyCommand.Path) {
        Split-Path $Invocation.MyCommand.Path
      } else {
        $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf(""))
      }
    }
    
    function cleanup
    {
      param(
        [System.Management.Automation.PSReference]$selenium_ref
      )
      try {
        $selenium_ref.Value.Quit()
      } catch [exception]{
        # Ignore errors if unable to close the browser
        Write-Output (($_.Exception.Message) -split "`n")[0]
    
      }
    }
    
    $shared_assemblies = @{
      'WebDriver.dll' = 2.44;
      'WebDriver.Support.dll' = '2.44';
      'nunit.core.dll' = $null;
      'nunit.framework.dll' = '2.6.3';
    
    }
    
    $shared_assemblies_path = 'c:\developer\sergueik\csharp\SharedAssemblies'
    
    if (($env:SHARED_ASSEMBLIES_PATH -ne $null) -and ($env:SHARED_ASSEMBLIES_PATH -ne '')) {
      $shared_assemblies_path = $env:SHARED_ASSEMBLIES_PATH
    }
    
    pushd $shared_assemblies_path
    $shared_assemblies.Keys | ForEach-Object {
      # http://all-things-pure.blogspot.com/2009/09/assembly-version-file-version-product.html
      $assembly = $_
      $assembly_path = [System.IO.Path]::Combine($shared_assemblies_path,$assembly)
      $assembly_version = [Reflection.AssemblyName]::GetAssemblyName($assembly_path).Version
      $assembly_version_string = ('{0}.{1}' -f $assembly_version.Major,$assembly_version.Minor)
      if ($shared_assemblies[$assembly] -ne $null) {
        # http://stackoverflow.com/questions/26999510/selenium-webdriver-2-44-firefox-33
        if (-not ($shared_assemblies[$assembly] -match $assembly_version_string)) {
          Write-Output ('Need {0} {1}, got {2}' -f $assembly,$shared_assemblies[$assembly],$assembly_path)
          Write-Output $assembly_version
          throw ('invalid version :{0}' -f $assembly)
        }
      }
    
      if ($host.Version.Major -gt 2) {
        Unblock-File -Path $_;
      }
      Write-Debug $_
      Add-Type -Path $_
    }
    popd
    
    $verificationErrors = New-Object System.Text.StringBuilder
    
    $hub_port = '4444'
    $uri = [System.Uri](('http://{0}:{1}/wd/hub' -f $hub_host,$hub_port))
    
    try {
      $connection = (New-Object Net.Sockets.TcpClient)
      $connection.Connect($hub_host,[int]$hub_port)
      $connection.Close()
    } catch {
      Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "start cmd.exe /c c:\java\selenium\selenium.cmd"
    
      Start-Sleep -Seconds 3
    }
    [object]$profile_manager = New-Object OpenQA.Selenium.Firefox.FirefoxProfileManager
    
    [OpenQA.Selenium.Firefox.FirefoxProfile]$selected_profile_object = $profile_manager.GetProfile($profile)
    [OpenQA.Selenium.Firefox.FirefoxProfile]$selected_profile_object = New-Object OpenQA.Selenium.Firefox.FirefoxProfile ($profile)
    $selected_profile_object.setPreference('general.useragent.override','Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16')
    
    $selenium = New-Object OpenQA.Selenium.Firefox.FirefoxDriver ($selected_profile_object)
    [OpenQA.Selenium.Firefox.FirefoxProfile[]]$profiles = $profile_manager.ExistingProfiles
    
    
    $DebugPreference = 'Continue'
    $base_url = 'https://codeproject.org.cn/'
    
    $selenium.Manage().Window.Size = @{ 'Height' = 600; 'Width' = 480; }
    $selenium.Manage().Window.Position = @{ 'X' = 0; 'Y' = 0 }
    
    $selenium.Navigate().GoToUrl($base_url)
    set_timeouts ([ref]$selenium)
    
    $css_selector = 'span.member-signin'
    Write-Debug ('Trying CSS Selector "{0}"' -f $css_selector)
    [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(1))
    
    try {
    
      [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementExists([OpenQA.Selenium.By]::CssSelector($css_selector)))
    
    } catch [exception]{
      Write-Output ("Exception with {0}: {1} ...`n(ignored)" -f $id1,(($_.Exception.Message) -split "`n")[0])
    }
    Write-Debug  ('Found via CSS Selector "{0}"' -f $css_selector )
    
    # highlight the element
    [OpenQA.Selenium.IWebElement]$element = $selenium.FindElement([OpenQA.Selenium.By]::CssSelector($css_selector))
    [OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteScript("arguments[0].setAttribute('style', arguments[1]);",$element,'border: 2px solid red;')
    Start-Sleep 3
    [OpenQA.Selenium.IJavaScriptExecutor]$selenium.ExecuteScript("arguments[0].setAttribute('style', arguments[1]);",$element,'')
    
    # Click on the element:
    [OpenQA.Selenium.Interactions.Actions]$actions = New-Object OpenQA.Selenium.Interactions.Actions ($selenium)
    
    try {
    $actions.MoveToElement([OpenQA.Selenium.IWebElement]$element).Click().Build().Perform()
    
    } catch [OpenQA.Selenium.WebDriverTimeoutException]{
      # Ignore
      # 
      # Timed out waiting for async script result  (Firefox)
      # asynchronous script timeout: result was not received (Chrome)
      [NUnit.Framework.Assert]::IsTrue($_.Exception.Message -match '(?:Timed out waiting for page load.)')
    }
    
    $input_name = 'ctl01$MC$MemberLogOn$CurrentEmail'
      [OpenQA.Selenium.Support.UI.WebDriverWait]$wait = New-Object OpenQA.Selenium.Support.UI.WebDriverWait ($selenium,[System.TimeSpan]::FromSeconds(1))
      $wait.PollingInterval = 100
    
      $xpath = ( "//input[@name='{0}']"  -f  $input_name)
      Write-Debug ('Trying XPath "{0}"' -f $xpath)
    
    try {
      [void]$wait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementIsVisible([OpenQA.Selenium.By]::XPath($xpath)))
    } catch [exception]{
      Write-Output ("Exception with {0}: {1} ...`n(ignored)" -f $id1,(($_.Exception.Message) -split "`n")[0])
    }
      Write-Debug ('Found XPath "{0}"' -f $xpath)
    
    [OpenQA.Selenium.IWebElement]$element = $selenium.FindElement([OpenQA.Selenium.By]::XPath($xpath))
    [NUnit.Framework.Assert]::IsTrue($element.GetAttribute('type') -match 'email')
    $email_str = 'kouzmine_serguei@yahoo.com'
    $element.SendKeys($email_str)
    
    
    
    # Do not close Browser / Selenium when run from Powershell ISE
    if (-not ($host.name -match 'ISE') ) {
    
    if ($PSBoundParameters['pause']) {
      try {
        [void]$host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
      } catch [exception]{}
    
    } else {
      Start-Sleep -Millisecond 1000
    }
    
    # Cleanup
     cleanup ([ref]$selenium)
    }

    让我们剖析这个脚本。以下屏幕截图说明了该过程。

    ISE

    极端情况

    为了举例说明 C# 和 PowerShell 之间相对较大的语法差异,考虑转换处理 IPv4 地址输入字段的自定义输入元素处理程序,摘自 Mervick 的 C# 初学者 IPBox 文章。

    C# 版本(片段)

    		private void OnTextChange(object sender, System.EventArgs e)
    		{
    			int box_type = 0;
    
    			CultureInfo MyCultureInfo = new CultureInfo("en-GB");
    
    			double d;
    
    			if( sender.Equals( ip1 ) )
    				box_type = 1;
    			if( sender.Equals( ip2 ) )
    				box_type = 2;
    			if( sender.Equals( ip3 ) )
    				box_type = 3;
    			if( sender.Equals( ip4 ) )
    				box_type = 4;
    
    			switch( box_type )
    			{
    				case 1:
    
    					if( this.ip1.Text.Length > 0 && this.ip1.Text.ToCharArray()[this.ip1.Text.Length - 1] == '.' )
    					{
    						this.ip1.Text = this.ip1.Text.TrimEnd( '.' );
    						ip1.Text = (this.ip1.Text.Length > 0 ) ? int.Parse( this.ip1.Text ).ToString() : "0" ;
    						ip2.Focus();
    						return;
    					}
    
    					// integer validation
    					if( double.TryParse(
    						this.ip1.Text,
    						System.Globalization.NumberStyles.Integer,
    						MyCultureInfo,
    						out d ) == false
    						)
    					{
    						this.ip1.Text = this.ip1.Text.Remove( 0, this.ip1.Text.Length );
    						return;
    					}
    
    					// change focus to the next textbox if fully inserted
    					if( this.ip1.Text.Length == 3 )
    					{
    						if( int.Parse( this.ip1.Text ) >= 255 )
    							this.ip1.Text = "255";
    						else
    							ip1.Text = int.Parse( ip1.Text ).ToString();
    						ip2.Focus();
    					}
    					break;
    				case 2:
    ...

    等效的 PowerShell 版本。

    function text_changed () {
    param(
    [object]$sender,
    [System.EventArgs]$eventargs
    )
    [int]$box_type = 0
    [System.Globalization.CultureInfo]$ci = New-Object System.Globalization.CultureInfo ("en-GB")
    [double]$d = 0
    if ($sender -eq $ip1) {
    $box_type = 1 }
    if ($sender -eq $ip2) {
    $box_type = 2 }
    if ($sender -eq $ip3) {
    $box_type = 3 }
    if ($sender -eq $ip4) {
    $box_type = 4 }
    switch ($box_type)
    {
    1 {
    if (($ip1.Text.Length -gt 0) -and ($ip1.Text.ToCharArray()[$ip1.Text.Length - 1] -eq '.'))
    {
    $ip1.Text = $ip1.Text.TrimEnd('.')
    if ($ip1.Text.Length -gt 0) {
    $ip1.Text = [int]::Parse($ip1.Text).ToString()
    } else {
    $ip1.Text = '0'
    }
    $ip2.Focus()
    return
    }
    # integer validation
    if ([double]::TryParse(
    $ip1.Text,
    [System.Globalization.NumberStyles]::Integer,
    $ci,
    ([ref]$d)) -eq $false
    )
    {
    $ip1.Text = $ip1.Text.Remove(0,$ip1.Text.Length)
    return
    }
    # change focus to the next textbox if fully inserted
    if ($ip1.Text.Length -eq 3) {
    if ([int]::Parse($ip1.Text) -ge 255) {
    $ip1.Text = '255'
    } else {
    $ip1.Text = [int]::Parse($ip1.Text).ToString()
    }
    $ip2.Focus()
    }
    }
    2 {
    ...

    IpBox

    在此示例中,可能应该避免转换。完整的脚本源代码可在源 zip 文件中找到。

    剖析过程

    初步讨论

    在本节中,我们将 C# 代码分步转换为可运行的 PowerShell 脚本,分 3 步,然后是另外 2 步。

    • http://www.java2s.com/Code/CSharp/GUI-Windows-Form/MyClockForm.htm 下载代码,将其保存为文本文件 timer.cs。编译并确保其在控制台中运行。

      C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe timer.cs
      
      invoke-expression -command './timer.exe'
    • 创建一个空白文本文件 timer_iter1.ps1,并在其中放入以下样板代码:

      Add-Type -TypeDefinition @"
      // -- about to paste the c# code below. Any class would do
      "@ -ReferencedAssemblies 'System.Windows.Forms.dll', 'System.Drawing.dll', 'System.Data.dll', 'System.ComponentModel.dll'
      
      $clock = New-Object MyClock.MyClockForm
      $clock.ShowDialog()
      $clock.Dispose()

      检查要转换的类的命名空间和类名,确保 PowerShell 创建的是同一类的实例。

      namespace MyClock
      {
      public class MyClockForm : System.Windows.Forms.Form {
      /// implementation
      } }

      因此 New-Object MyClock.MyClockForm

      找出 C# 类“using”区域中所需的程序集。

      using System;
      using System.Drawing;
      using System.Collections;
      using System.ComponentModel;
      using System.Windows.Forms;
      using System.Data;

      将类的代码粘贴到 PowerShell 脚本 Add-Type cmdlet TypeDefinition 的文本参数中,并确保其可运行。

      . ./timer_iter1.ps1
    • 如果收到错误

      Add-Type : Cannot add type. The type name 'Win32Window' already exists.

      PowerShell 窗口需要重启。当然,如果您收到

      Add-Type : Cannot add type. Compilation errors occurred.
      FullyQualifiedErrorId : SOURCE_CODE_ERROR,

      您需要修复代码。

      PowerShell 版本的类应该看起来和感觉与编译后的可执行文件一样,但显然还没有在脚本和对话框之间共享数据的明显方法。

    • 现在将脚本过程显式地转变为对话框的caller

      请注意,http://msdn.microsoft.com/en-us/library/system.windows.forms.form.showdialog(v=vs.90).aspx 描述了 ShowDialog 方法的两个备用签名,每个 Windows 窗体都响应。后者接受所有者对象。

      ShowDialog(IWin32Window) 将窗体显示为具有指定调用者的模态对话框。

      任何实现 IWin32Window 的类都可以成为任意 Windows 窗体内部的模态对话框的所有者。

      因此,我们使用一个纯 C# 对象代码源重复之前的 Add-Type 代码混合练习。

      Add-Type -TypeDefinition @"
      
      // "
      using System;
      using System.Windows.Forms;
      public class Win32Window : IWin32Window
      {
          private IntPtr _hWnd;
          private int _data;
          private string _message;
      
          public int Data
          {
              get { return _data; }
              set { _data = value; }
          }
          public string Message
          {
              get { return _message; }
              set { _message = value; }
          }
      
          public Win32Window(IntPtr handle)
          {
              _hWnd = handle;
          }
      
          public IntPtr Handle
          {
              get { return _hWnd; }
          }
      }
      
      "@ -ReferencedAssemblies 'System.Windows.Forms.dll'

      上面的代码实现了接口IWin32Window 所需的单个方法——带窗口句柄的构造函数。上面代码中的其他属性 DataMessage 不是接口所必需的,但对于将各部分连接在一起至关重要。

    • 最后,修改代码以处理调用者。

      • 将参数传递给 Windows.Forms
        $process_window = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle )
        
          $timer.ShowDialog([Win32Window ] ($process_window) ) | out-null
        
        write-output $process_window.GetHashCode()
      • 从窗体内部访问对象。

        您需要为该类添加一个成员变量,并修改以下两个方法。请注意,在实现 PowerShell 版本时不需要这样做。一定有更好的方法来演示这一点。现在,目标是迁移到 PowerShell 版本,并最终丢弃修改后的类。这种“理由”上的 hack。

        private void OnTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
                {
                    // The interval has elapsed and this timer function is called after 1 second
                    // Update the time now.
                    label1.Text = DateTime.Now.ToString();
        
                    label1.Text = String.Format("My Clock {0} {1}", caller.ToString(), caller.GetHashCode() );
                }
        
            public new DialogResult ShowDialog(IWin32Window caller){
                this.caller = caller ;
                return base.ShowDialog(caller);
            }

        另一方面,当要移植的代码比此示例更复杂的窗体时,通过同一个对象 $caller 交换所有特定领域的数据将会很有帮助,无论其复杂性如何。您可以 Visual Studio 或 PowerShell ISE 中测试管道的任何一侧,并模拟另一侧,而不必过多担心细节。

      将代码另存为 timer_iter2.ps1 并确认它仍然运行。

      运行脚本会产生相同的对象,该对象可供脚本和窗体使用。

      Dialog result

    实际转换为 PowerShell

    下一步是选择性地重写窗体的方法和元素,并移除“混合”代码。让 C# 编译器接受 $caller 响应许多额外数据消息的事实并不容易。另一种选择是使用反射,但不会产生简洁或漂亮的 PING。

    所需代码编辑都是语义上的。

    • 移除实例引用(this)以及类声明、构造函数、命名空间等。成员 this.timer1 变成 $timer1,依此类推。this 变成简单的 $f——窗体对象。
    • 修改方法调用的语义:new System.Timers.Timer(); 变成 new-object System.Timers.Timer,等等。当在方法调用参数中找到类实例化时,似乎可以将嵌套方法调用分开。
    • 更改常量解析的语义:System.Drawing.ContentAlignment.MiddleCenter 将变为 [System.Drawing.ContentAlignment]::MiddleCenter 等。始终提供完全解析的类名:ImageList il = new ImageList(); 将必须变为 $il = new-object System.Windows.Forms.ImageList 等。如果不确定,请查阅 MSDN。
    • 注意细微的语义差异,例如 -eq 代替 ==-bor 代替 | 等。
    • 首先运行可视化布局,但注释掉事件传播。一旦窗体开始显示,再处理事件。

      确保事件处理程序*在*使用它们之前定义好:例如,将以下代码的前几行移到顶部

      $button1_Click = {
         param(
          [Object] $sender,
          [System.EventArgs] $eventargs
          )
          [System.Windows.Forms.MessageBox]::Show('hello');
      }
      $button1.Add_Click($button1_Click)

      这样,当 $button1 被点击时,窗体将不再显示空白的 messagebox

    • 创建一个包装的 PowerShell 函数,并添加代码来使窗体可见。
      $f.ResumeLayout($false)
      $f.Topmost = $true
      $f.Activate()
      $f.Displose()

      $callershowDialog(...) 移到 PowerShell 函数内部。

      $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
      [void] $f.ShowDialog([Win32Window ] ($caller) )

    结果将如下所示

    function exampleTimer(
        [Object] $caller= $null
        )
    {
    $f = New-Object System.Windows.Forms.Form
    $f.Text = $title
    
    $timer1 = new-object System.Timers.Timer
    $label1 = new-object System.Windows.Forms.Label
    
    
    $f.SuspendLayout()
    $components = new-object System.ComponentModel.Container
    $label1.Font = new-object System.Drawing.Font("Microsoft Sans Serif", 14.25, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Point, [System.Byte]0);
    $label1.ForeColor = [System.Drawing.SystemColors]::Highlight
    $label1.Location = new-object System.Drawing.Point(24, 8)
    $label1.Name = "label1"
    $label1.Size = new-object System.Drawing.Size(224, 48)
    $label1.TabIndex = 0;
    $label1.Text = [System.DateTime]::Now.ToString()
    $label1.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
    
    $f.AutoScaleBaseSize = new-object System.Drawing.Size(5, 13)
    $f.ClientSize = new-object System.Drawing.Size(292, 69)
    $f.Controls.AddRange(@( $label1))
    $f.Name = 'MyClockForm';
    $f.Text = 'My Clock';
    
    # This was added - it does not belong to the original Form
    $eventMethod=$label1.add_click
    $eventMethod.Invoke({$f.Text="You clicked my label $((Get-Date).ToString('G'))"})
    
    # This silently ceases to work
    $f.Add_Load({
      param ([Object] $sender, [System.EventArgs] $eventArgs )
        $timer1.Interval = 1000
        $timer1.Start()
        $timer1.Enabled = $true
    
    })
    
    $timer1.Add_Elapsed({
         $label1.Text = [System.DateTime]::Now.ToString()
    })
    
    # This loudly ceases to start the timer "theTimer"
    $global:timer = New-Object System.Timers.Timer
    $global:timer.Interval = 1000
    Register-ObjectEvent -InputObject $global:timer -EventName Elapsed -SourceIdentifier theTimer -Action {AddToLog('') }
    $global:timer.Start()
    $global:timer.Enabled = $true
    
    function AddToLog()
    {
    param ([string] $text )
    
         $label1.Text = [System.DateTime]::Now.ToString()
    }
    
    $f.ResumeLayout($false)
    $f.Topmost = $True
    
    if ($caller -eq $null ){
      $caller = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    }
    
    $f.Add_Shown( { $f.Activate() } )
    $f.ShowDialog([Win32Window] ($caller) )
    
    }

    这样几乎所有东西都就绪了,除了事件处理程序似乎没有被触发——时间戳未更新。这段代码显然需要修复。

    调试 Timer 问题

    经过一些调试,发现脚本没有正确处理 Windows.Form 类实例曾经拥有但现在不再拥有的 Timer 对象。这是一个需要修复的独立问题,目前正在处理中。为了证明*大多数*事件处理程序*几乎可以*毫不费力地转换为运行 PowerShell 代码,我们向 label 添加了 click 处理程序。

    $eventMethod=$label1.add_click
    $eventMethod.Invoke({$f.Text="You clicked my label $((Get-Date).ToString('G'))"})

    PromptForChoice

    并进行了点击。结果符合预期。

    总结一下,根据 C# 蓝图编写等效的 PowerShell 代码来布局窗体以及处理事件,是本章前面承诺的最后两个步骤。

    可视化设计复制步骤显然是轻而易举的,最多只是一个打字练习。有了 Windows Presentation Foundation 甚至更简单:可以直接加载相同的 XAML。

    相反,事件管理可能需要一些精力来掌握。

    在本文档的 PowerShell 示例中,每次都尝试了略有不同的事件处理代码语义。这种多样性是故意引入的——所有变体都是等效的——.NET Framework 在后台生成大量代码来支持 MulticastDelegate

    总结一下,根据 C# 蓝图在 PowerShell 中复制可视化设计以及处理事件,是本章前面承诺的最后两个步骤。可视化设计步骤轻而易举,最多只是一个打字练习。相反,事件管理可能需要一些精力来掌握。在本文档的 PowerShell 示例中,每次都选择了略有不同的事件处理代码语义。这种多样性是故意引入的——所有变体都是等效的。在后台,MS .NET 会生成大量代码来继承 MulticastDelegate

    PromptForChoice

    PowerShell 内置的提示机制主要用于控制破坏性操作。其确切的呈现方式取决于运行 PowerShell 脚本的主机。在 http://technet.microsoft.com/en-us/library/ff730939.aspx 中为基本的“是?否?也许”多项选择建议的无限循环解决方案几乎是不可接受的。它传递了一个明确的信息:“放弃多选提示”。

    $heads = New-Object System.Management.Automation.Host.ChoiceDescription "&Heads", "Select Heads."
        $tails = New-Object System.Management.Automation.Host.ChoiceDescription "&Tails", "Select Tails."
        $cancel = New-Object System.Management.Automation.Host.ChoiceDescription "&Cancel", "Skip to the next step."
        $options = [System.Management.Automation.Host.ChoiceDescription[]]($heads, $tails, $cancel)
        $host.ui.PromptForChoice("Call it","----", $options,2 )

    PromptForChoice

    它根据 ConsoleHostWindows PowerShell ISE Host 中的*主机*功能呈现不同的效果。

    PromptForChoice

    并返回所选选项的索引 - 0, 1, 2。

    平台兼容性

    本文档中的 PowerShell 脚本已在以下平台验证通过:

    Windows Server 2012 - 桌面体验
    Windows Server 2012 - 最小服务器界面,Windows Server 2012 - Windows Server Core 大多数示例都能正常工作,但有一个例外:toggle_display.ps1 可以显示窗体,然后隐藏,但从未能重新显示 PowerShell 控制台。
    Windows Server 2008 R2
    Windows Server 2008
    Windows Server 2003
    Windows 8 ?
    Windows 7
    Windows Vista
    Windows XP
    Windows 2000

    历史

    最初的工作是自动化日常的 DevOps 例程,配置包含 Microsoft 软件的标准化 UAT 环境,托管在私有云中。其中一个特别繁琐的步骤是通过 SQL Server 客户端网络配置实用程序选择性地克隆 SQL 配置。后者非常不用户友好。

    cliconfg.exe

    在后台,所有信息都存储在一个注册表项中。这使得从远程主机加载这些信息成为自动化的良好候选,但操作员的角色仍然至关重要,因为环境景观之间存在细微差别:哪些 IIS 应用程序托管在哪台计算机上。如果设置被转换为类似 Puppet 的节点定义,这本不是问题。

    GitHub 上的源代码

    对于大多数示例,完整的源代码在本文章和附带的 zip 文件中提供。也可以从 Github 克隆完成的源代码。

    发布历史

    • 2014-07-21 - 初始版本
    • 2014-07-21 - 添加了更多示例
    • 2014-07-22 - 添加了关于代码转换的注释
    • 2014-07-22 - 添加了 XAML 示例
    • 2014-07-23 - 添加了 TreeView 示例
    • 2014-07-24 - 添加了 Dissect Conversion 示例
    • 2014-07-25 - 添加了带有 Treeview 的自定义图标
    • 2014-07-25 - 添加了关于 Get-Credential cmdlet 的说明
    • 2014-07-26 - 添加了 TabControl 和 Focus 示例
    • 2014-07-26 - 添加了目录
    • 2014-07-26 - 添加了 Tabbed Treeviews
    • 2014-07-26 - 重构了示例代码片段
    • 2014-07-27 - 添加了 WebBrowser1 示例
    • 2014-07-27 - 添加了平台兼容性矩阵
    • 2014-07-28 - 添加了即时生成 XAML 对话框的示例
    • 2014-07-29 - 添加了脚本参数提示 DataGridView 示例
    • 2014-07-29 - 添加了填充颜色和 ZIndex 操作示例
    • 2014-07-29 - 添加了 WPF Form 文本操作示例
    • 2014-07-29 - 添加了双向 Form Script 文本通信示例
    • 2014-08-09 - 添加了 Selenium Script 示例
    • 2014-08-09 - 修改了 Selenium Grid Test 示例以在 Safari 浏览器上执行
    • 2014-08-09 - 添加了关于文件下载对话框处理的说明
    • 2014-08-10 - 添加了带有 ComboBox 的 TreeView 控件示例
    • 2014-08-10 - 添加了代码格式化缺陷的解决方法
    • 2014-08-11 - 添加了 ProgressBar 示例
    • 2014-08-13 - 添加了 Selenium IE 对话框处理器示例
    • 2014-08-13 - 修复了格式并分离了一些内联 XAML 代码以提高可读性
    • 2014-08-16 - 添加了 Selenium IDE Powershell Formatter 示例
    • 2014-08-16 - 更新了指向作者 Powershell Selenium IDE Formatter git 仓库的链接
    • 2014-08-19 - 添加了拖放示例
    • 2014-08-22 - 添加了通过 Selenium 运行 Javascript 的示例
    • 2014-08-22 - 添加了 Microsoft Test Agent DLL 发现示例
    • 2014-08-22 - 添加了 xpi 的概述和构建说明
    • 2014-08-23 - 添加了点击“保存”对话框按钮的示例
    • 2014-08-23 - 添加了从 Linux 运行 Powershell 的示例
    • 2014-08-24 - 更新了“保存”对话框示例版本,以接受指定的下载文件路径
    • 2014-09-03 - 添加了 WebDriver 拖放示例
    • 2014-09-09 - 添加了杂项 WebDriver 示例
    • 2014-09-09 - 添加了隐藏 Powershell 控制台窗口的示例
    • 2014-09-09 - 添加了关于 Windows Server Core 中 Powershell UI 的说明
    • 2014-09-21 - 添加了柱状图 (VB.Net) 示例
    • 2014-09-24 - 添加了上下选择器示例
    • 2014-09-26 - 添加了超时确认对话框示例
    • 2014-10-07 - 添加了极端情况示例,恢复了几个损坏的段落,执行了少量 HTML 格式清理
    • 2014-10-07 - 添加了 Selenium SendKeys 示例
    • 2014-10-07 - 恢复了 Selenium IDE Powershell Formatter 部分
    • 2014-10-07 - 恢复了 DropDown ComboBox 部分
    • 2014-11-01 - 添加了文件系统 Treeview 示例
    • 2014-11-03 - 更新了包含最终文件系统 Treeview 和自定义 MsgBox 示例的 Source Zip
    • 2014-11-04 - 添加了自定义 MsgBox 示例
    • 2014-11-14 - 添加了 Ribbon 示例
    • 2014-11-14 - 添加了 Selenium Powershell ISE 示例
    • 2014-12-07 - 添加了可折叠列表示例
    • 2014-12-14 - 添加了已选组合列表框示例
    • 2014-12-20 - 添加了饼图和柱状图绘制示例
    • 2014-12-22 - 添加了 Timer 示例
    • 2015-01-04 - 添加了任务列表进度示例
    • 2015-01-05 - 评论了任务列表进度
    • 2015-01-14 - 添加了手风琴菜单示例
    • 2015-01-14 - 添加了手风琴菜单代码重构示例
    • 2015-01-17 - 添加了圆形进度指示器示例
    • 2015-01-19 - 添加了圆形进度指示器 W2K3 兼容性补丁
    • 2015-02-07 - 重构了 Ribbon 按钮示例
    • 2015-02-15 - 添加了 Selenium 调试消息到 Explorer 任务栏的示例
    • 2015-02-16 - 添加了 Selenium EventFiring WebDriver 示例 *WIP
    • 2015-02-17 - 修复了格式缺陷
    • 2015-02-27 - 添加了 TreeTabControl 示例
    • 2015-02-27 - 继续 TreeTabControl 示例 *WIP
    • 2015-03-10 - 添加了替代的 Add-Type 语法示例。删除了空行。
    • 2015-03-22 - 提供了替代的 $script: 语法示例并上传了拼写修复。
    • 2015-03-23 - 添加了关于 System.Management.Automation.TypeAccelerators 的说明。
    • 2015-03-25 - 添加了测试配置显示示例。
    • 2015-04-04 - 替换并简化了自定义调试消息框示例。
    • 2015-04-05 - 添加了 OS X 圆形进度指示器示例。
    • 2015-04-10 - 添加了可排序的 ListView 示例。
    • 2015-04-17 - 添加了填充 GridView 示例。
    • 2015-05-31 - 添加了通用对话框示例。
    • 2015-12-26 - 添加了图表 FiddlerCore 和 Selenium window.performance.getEntries() 示例的真实世界数据。
    • 2015-12-26 - 添加了三态 Tree View 示例。
    • 2016-01-14 - 添加了三态 Tree View 示例。
    • 2016-02-02 - 更新了三态 Tree View 示例和下载。
© . All rights reserved.