通过基本 Windows 窗体处理 PowerShell 输入
本文档给出了一些在 PowerShell 脚本中嵌入基本 Windows 窗体和 WPF 以收集用户输入的示例。
目录
- 引言
- PromptForChoice ?!
- 背景
- Using the Code
- 多项选择提示
- 超时提示
- 收集复选框和单选按钮组的选择
- 选中列表框选择
- 手风琴菜单
- 选中组合框
- 条形图
- 图表的真实世界数据
- 折线图、条形图和饼图
- 数据网格概念验证
- 列表视图
- 填充 GridView DataTable
- 带可折叠组的列表
- 拖放
- 上下
- 功能区按钮
- 自定义调试消息框
- 杂项。密码输入
- 通用对话框
- 带输入焦点控制的制表对话框
- 进度条
- 定时器
- 任务列表进度
- 圆形进度指示器
- 文件系统树视图
- 嵌入 XAML
- 连接 WPF 事件
- 树视图
- 系统托盘通知图标
- Selenium 测试
- Selenium IDE PowerShell 格式化程序
- 通用 Selenium 自动化
- 使用 Selenium sendKeys 上传文件
- 杂项。WebDriver 用法
- 在 Explorer 任务栏上显示 Selenium 调试消息
- Selenium EventFiring WebDriver 示例
- 杂项。实用程序
- 在 PowerShell ISE 中编写 Selenium 脚本
- 极端情况
- 剖析过程
- GitHub 上的源代码
- 历史
引言
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
多项选择提示
多项选择决策提示是最简单的示例,它不需要窗体元素之间进行任何通信——窗体在每个按钮的 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.Timer
的 System.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
的属性和方法是公共的,因此脚本提供了事件处理程序——在上面的示例中,一分钟的间隔(以秒为单位)被硬编码了。
完整的示例显示在下面,并在源 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)
收集复选框和单选按钮组的选择
此示例代码更有趣,因为脚本将收集多个分组元素的的状态。管理单个 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
}
列表框选择
下一个迭代是让窗体从 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 中。
$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()
为了对抗冗余,可以引入实用程序函数,例如
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++
}
在窗体委托中,迭代引用数据的键,并清除/设置哈希值。
$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
})
条形图
下一个示例显示了自定义绘制的条形图,它没有第三方图表库依赖。使用了来自 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 打开窗体并将两个数据样本发送给它,在每个样本渲染后等待几秒钟,然后关闭窗体。
$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()
已向该窗体添加了两个公共方法 LoadData
和 RenderData
以允许从脚本控制窗体。为了防止修改原始示例,第一个方法克隆了来自调用者的数据,而后者创建了一个虚拟的事件参数并调用了处理程序。
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# 类中。
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()
}
调用者通过引用传递数据。
$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))
数据网格概念验证
网格无疑是提供给用户操作的最复杂对象。
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()
}
在按钮处理程序中,我们防止窗体关闭,直到没有空白参数为止。输入焦点会移到期望输入单元格。为简单起见,此处我们接受所有参数的文本输入,而不考虑其类型。
列表视图
现在假设您运行一系列松散的(例如 Selenium)测试,这些测试使用 Excel 文件作为测试参数和结果。
读取设置
$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 )
}
}
完整的脚本源代码可在源 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);
}
}
}
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;
}
请注意,修改 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)
}
拖放
下一个示例涵盖了拖放列表框。有大量的事件需要处理,并且将 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
窗体相应地调整了光标——这在屏幕截图中未显示。窗体关闭后,脚本会打印选定的项目。这样的控件可能很有用,例如安排 Selenium 测试到子集中(转换为和从 *.orderedtests
资源未显示)。完整的脚本源代码可在源 zip 文件中找到。
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;
}
$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)
功能区按钮
您可以改编 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)
并显示带有功能区按钮的窗体。
$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
}
}
}
稍作修改以显示异常对话框。
以及调用堆栈信息,并可选地继续执行。
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
}
您可以使用此片段来处理常规异常。
或各种按钮组合。完整示例可在源 zip 文件中找到(两个版本:一个保留原始 C# 代码,一个简化版)。
杂项。密码
纯文本
现在,假设任务需要向源代码控制、CI 或其他使用自己的身份验证机制且不接受 NTLM 的远程服务进行身份验证。以下代码有助于提示用户名/密码。它使用了标准的 Windows 窗体实践,即屏蔽密码文本框。
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()
}
在此脚本中,我们将 User
和 password
存储在单独的字段中。
$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
用于凭据验证、管理员权限、修改新安装的服务的代码已从显示中省略。
会话 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# 代码。
并在 $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) )
}
$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
}
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
}
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
}
@( '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
下显示警告消息。
提供输入后,警告消息会被清除。
负责此功能的代码突出显示如下:
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()
之间存在细微差别,此处未涵盖。
单击按钮会启动一个 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()
}
出于调试目的,窗体上添加了带有相同处理程序的 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
包含进度条和计时器的窗体完全用 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()
一切完成后,窗体关闭,运行空间被销毁。
如果您要修改 Ibenza.UI.Winforms.ProgressTaskList
的源代码,首先将类的设计器生成的代码存储在脚本中作为 Add-Type
的 TypeDefinition
参数。唯一需要的修改是从 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
对象来存储 Current
和 Last
索引。
圆形进度指示器
下一个示例结合了 异步 GUI 和 ProgressCircle-进度控件,以生成一个通过 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()
文件系统树视图
下一个示例将 文件系统树视图 自定义为 PowerShell。在 Add-Type -TypeDefinition
中,结合了 FileSystemTreeView
和 ShellIcon
类的实现。
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
。
$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
设计 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
}
产生
并以 PowerShell 的方式打印选定的结果。
$target.ShowDialog() | out-null
write-output 'Selected items:'$items | where-object {$selected.ContainsKey( $_ ) }
更多
值得注意的是,您可以在纯 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
可以获得独特的效果。
但是设计代码隐藏可能很困难。安排 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!'
}
此示例初始化文本时包含一些拼写错误。
并等待用户修复拼写错误。一旦文本被更正或超时到期,窗体将关闭并打印摘要。
由于 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()
在此示例中,使用 ToolTip
Opened,Closed
事件通过 Synchronized
将 NeedData
标志设置和清除到顶层脚本,然后将文本更改为 please wait
并显示沙漏直到数据准备好。数据渲染再次在 send_text
中执行。请注意,send_text
函数现在调用 Dispatcher
两次,视觉反馈不完美。每次鼠标离开和重新进入 Tooltip
激活区域时,都会请求并提供新数据。
树视图
纯文本
在启动 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)
}
并为各个节点使用不同的图标。使用相同的技术,调用脚本可以描述要为每个节点渲染的图标。
$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()
}
代码仍在进行中,目的是使用状态标签进行验证警告,并使用工作进程进行更深入的环境验证。
下拉组合框
要在 V4 之前的 PowerShell 环境中管理 PowerShell Desired State Configuration 配置管理器 - 节点 - 提供程序 - 属性输入,您可能希望使用 combobox
扩展 treeview
。例如,可以使用 Mattman206 的自定义带组合框下拉节点的树视图控件。在编译类并将程序集放置在 SHARED_ASSEMBLIES_PATH
文件夹后,脚本会加载它,并在表单加载事件处理过程中自由混合 System.Windows.Forms.TreeNode
和 DropDownTreeView.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()
}
代码仍在进行中,目的是使用状态标签进行验证警告,并使用工作进程进行更深入的环境验证。
三态树视图
PowerShell 允许管理员管理大量数据,因此通常希望更新复杂的对象集合,而三态树视图可能是首选。下面的示例包装了 RikTheVeggie 的自定义 TriStateTreeView
类。实际上,源代码 TriStateTreeView.cs
未经修改地嵌入到脚本中——唯一需要修改的部分是测试示例中的 PopulateTree
和 triStateTreeView1_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)
}
}
})
一个更实用的示例结合了 三态树视图 和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>
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 提供的系统托盘通知图标示例,您可以让主脚本通过气球提示消息和控制台显示其状态,并且构建日志文件用于渲染托盘图标菜单并向其传递其他信息。
#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'
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-ScriptDirector
y 和 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)
并完成测试运行。
try {
$driver.Quit()
} catch [Exception] {
# Ignore errors if unable to close the browser
}
您可能会通过适当的 CreateRunspace
调用引入一个单独的脚本,并开发 Synchronized
对象以允许从连接到主脚本的单独 PowerShell 运行空间控制 $driver.GetScreenshot
调用的调用(这目前正在进行中),方式类似于先前示例中控制的系统托盘通知图标。
脚本的 Selenium RC 版本将加载不同的库并切换到 Nunit
库 Asserts
。
$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')
脚本的其余部分将保持不变。
当然,您可以在 PowerShell ISE 中直接创建脚本,这将节省大量开发时间。
要使用最新版本的 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 的类,并使用 EnumWindows
、GetWindowInfo
、EnumPropsEx
、GetProp
、GetWindowText
、GetWindowTextLength
、GetWindowThreadProcessId
win32 API 从 user32.dll
通过 [DllImport()]
并加载 Windows.h 中定义的许多必需结构来访问窗口句柄并调用 PostMessage
或 SendMessage
到所需的按钮,或者简单地调用 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;
}
请注意,如果不发送“Enter”键,Windows Explorer 将忽略后台输入的文本,并以原始位置/名称保存文件。
修改后的代码包含在归档文件中。只需进行最少的努力,即可将类集成到 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."
}
结果将可供调用脚本使用……
当业务运行混合 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
}
该脚本可以在除 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 上翻译文本。页面包含以下片段。
<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)
然后等待以下元素出现。
<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 语法并进行其他必要的调整。创建格式化程序所需的所有内容只有一个文件。
需要小心的一点是不要从基于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>'+
// ...
在开发后期,您将安排源代码以适合 xpi,并创建 chrome.manifest
、install.rdf
和 format-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
要使用格式化程序:
- 打开 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, '')
显然,这里的 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
}
此按钮用于显示调试消息和(进行中)暂停脚本的执行。
$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
完整源代码可在 zip 文件中找到。
Selenium EventFiring WebDriver 示例
以下是 Selenium
EventFiringWebDriver
从 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)
杂项。实用程序
您可以将 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()
函数操作来自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)
}
在 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)
}
极端情况
为了举例说明 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 {
...
在此示例中,可能应该避免转换。完整的脚本源代码可在源 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
cmdletTypeDefinition
的文本参数中,并确保其可运行。. ./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 所需的单个方法——带窗口句柄的构造函数。上面代码中的其他属性
Data
和Message
不是接口所必需的,但对于将各部分连接在一起至关重要。 -
最后,修改代码以处理调用者。
- 将参数传递给
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
并确认它仍然运行。运行脚本会产生相同的对象,该对象可供脚本和窗体使用。
- 将参数传递给
实际转换为 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()
将
$caller
和showDialog(...)
移到 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'))"})
并进行了点击。结果符合预期。
总结一下,根据 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 )
它根据 ConsoleHost
与 Windows PowerShell ISE Host
中的*主机*功能呈现不同的效果。
并返回所选选项的索引 - 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 配置。后者非常不用户友好。
在后台,所有信息都存储在一个注册表项中。这使得从远程主机加载这些信息成为自动化的良好候选,但操作员的角色仍然至关重要,因为环境景观之间存在细微差别:哪些 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
Treeview
s - 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 - 添加了通用对话框示例。
目录
- 引言
- PromptForChoice ?!
- 背景
- Using the Code
- 多项选择提示
- 超时提示
- 收集复选框和单选按钮组的选择
- 选中列表框选择
- 手风琴菜单
- 选中组合框
- 条形图
- 图表的真实世界数据
- 折线图、条形图和饼图
- 数据网格概念验证
- 列表视图
- 填充 GridView DataTable
- 带可折叠组的列表
- 拖放
- 上下
- 功能区按钮
- 自定义调试消息框
- 杂项。密码输入
- 通用对话框
- 带输入焦点控制的制表对话框
- 进度条
- 定时器
- 任务列表进度
- 圆形进度指示器
- 文件系统树视图
- 嵌入 XAML
- 连接 WPF 事件
- 树视图
- 系统托盘通知图标
- Selenium 测试
- Selenium IDE PowerShell 格式化程序
- 通用 Selenium 自动化
- 使用 Selenium sendKeys 上传文件
- 杂项。WebDriver 用法
- 在 Explorer 任务栏上显示 Selenium 调试消息
- Selenium EventFiring WebDriver 示例
- 杂项。实用程序
- 在 PowerShell ISE 中编写 Selenium 脚本
- 极端情况
- 剖析过程
- GitHub 上的源代码
- 历史
引言
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
多项选择提示
多项选择决策提示是最简单的示例,它不需要窗体元素之间进行任何通信——窗体在每个按钮的 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.Timer
的System.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
的属性和方法是公共的,因此脚本提供了事件处理程序——在上面的示例中,一分钟的间隔(以秒为单位)被硬编码了。完整的示例显示在下面,并在源 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)
收集复选框和单选按钮组的选择
此示例代码更有趣,因为脚本将收集多个分组元素的的状态。管理单个
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 }
列表框选择
下一个迭代是让窗体从 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 中。
$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()
为了对抗冗余,可以引入实用程序函数,例如
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++ }
在窗体委托中,迭代引用数据的键,并清除/设置哈希值。
$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 })
条形图
下一个示例显示了自定义绘制的条形图,它没有第三方图表库依赖。使用了来自 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 打开窗体并将两个数据样本发送给它,在每个样本渲染后等待几秒钟,然后关闭窗体。
$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()
已向该窗体添加了两个公共方法
LoadData
和RenderData
以允许从脚本控制窗体。为了防止修改原始示例,第一个方法克隆了来自调用者的数据,而后者创建了一个虚拟的事件参数并调用了处理程序。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# 类中。
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() }
调用者通过引用传递数据。
$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))
数据网格概念验证
网格无疑是提供给用户操作的最复杂对象。
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() }
在按钮处理程序中,我们防止窗体关闭,直到没有空白参数为止。输入焦点会移到期望输入单元格。为简单起见,此处我们接受所有参数的文本输入,而不考虑其类型。
列表视图
现在假设您运行一系列松散的(例如 Selenium)测试,这些测试使用 Excel 文件作为测试参数和结果。
读取设置
$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 ) } }
完整的脚本源代码可在源 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); } } }
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; } 请注意,修改
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) }
拖放
下一个示例涵盖了拖放列表框。有大量的事件需要处理,并且将 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
窗体相应地调整了光标——这在屏幕截图中未显示。窗体关闭后,脚本会打印选定的项目。这样的控件可能很有用,例如安排 Selenium 测试到子集中(转换为和从
*.orderedtests
资源未显示)。完整的脚本源代码可在源 zip 文件中找到。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; }
$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)
功能区按钮
您可以改编 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)
并显示带有功能区按钮的窗体。
$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 } } }
稍作修改以显示异常对话框。
以及调用堆栈信息,并可选地继续执行。
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 }
您可以使用此片段来处理常规异常。
或各种按钮组合。完整示例可在源 zip 文件中找到(两个版本:一个保留原始 C# 代码,一个简化版)。
杂项。密码
纯文本
现在,假设任务需要向源代码控制、CI 或其他使用自己的身份验证机制且不接受 NTLM 的远程服务进行身份验证。以下代码有助于提示用户名/密码。它使用了标准的 Windows 窗体实践,即屏蔽密码文本框。
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() }
在此脚本中,我们将
User
和password
存储在单独的字段中。$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
用于凭据验证、管理员权限、修改新安装的服务的代码已从显示中省略。
会话 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# 代码。并在
$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) ) }
$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 }
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 }
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 }
@( '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
下显示警告消息。提供输入后,警告消息会被清除。
负责此功能的代码突出显示如下:
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()
之间存在细微差别,此处未涵盖。单击按钮会启动一个
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() }
出于调试目的,窗体上添加了带有相同处理程序的
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
包含进度条和计时器的窗体完全用 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()
一切完成后,窗体关闭,运行空间被销毁。
如果您要修改
Ibenza.UI.Winforms.ProgressTaskList
的源代码,首先将类的设计器生成的代码存储在脚本中作为Add-Type
的TypeDefinition
参数。唯一需要的修改是从 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
对象来存储Current
和Last
索引。圆形进度指示器
下一个示例结合了 异步 GUI 和 ProgressCircle-进度控件,以生成一个通过 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()
文件系统树视图
下一个示例将 文件系统树视图 自定义为 PowerShell。在
Add-Type -TypeDefinition
中,结合了FileSystemTreeView
和ShellIcon
类的实现。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
处理程序,其中选定的 TreeNodeFullPath
被存储并写入文本框。$show_files_checkbox
复选框允许在运行时打开和关闭LoadFiles
。$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
设计 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 }
产生
并以 PowerShell 的方式打印选定的结果。
$target.ShowDialog() | out-null write-output 'Selected items:'$items | where-object {$selected.ContainsKey( $_ ) }
更多
值得注意的是,您可以在纯 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
可以获得独特的效果。
但是设计代码隐藏可能很困难。安排 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!' }
此示例初始化文本时包含一些拼写错误。
并等待用户修复拼写错误。一旦文本被更正或超时到期,窗体将关闭并打印摘要。
由于 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()
在此示例中,使用
ToolTip
Opened,Closed
事件通过Synchronized
将NeedData
标志设置和清除到顶层脚本,然后将文本更改为please wait
并显示沙漏直到数据准备好。数据渲染再次在send_text
中执行。请注意,send_text
函数现在调用Dispatcher
两次,视觉反馈不完美。每次鼠标离开和重新进入Tooltip
激活区域时,都会请求并提供新数据。树视图
纯文本
在启动 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) }
并为各个节点使用不同的图标。使用相同的技术,调用脚本可以描述要为每个节点渲染的图标。
$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() }
代码仍在进行中,目的是使用状态标签进行验证警告,并使用工作进程进行更深入的环境验证。
下拉组合框
要在 V4 之前的 PowerShell 环境中管理 PowerShell Desired State Configuration 配置管理器 - 节点 - 提供程序 - 属性输入,您可能希望使用
combobox
扩展treeview
。例如,可以使用 Mattman206 的自定义带组合框下拉节点的树视图控件。在编译类并将程序集放置在SHARED_ASSEMBLIES_PATH
文件夹后,脚本会加载它,并在表单加载事件处理过程中自由混合System.Windows.Forms.TreeNode
和DropDownTreeView.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() }
代码仍在进行中,目的是使用状态标签进行验证警告,并使用工作进程进行更深入的环境验证。
制表项的树
下一个示例使用了漂亮的 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>
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 提供的系统托盘通知图标示例,您可以让主脚本通过气球提示消息和控制台显示其状态,并且构建日志文件用于渲染托盘图标菜单并向其传递其他信息。
#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'
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-ScriptDirector
y 和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)
并完成测试运行。
try { $driver.Quit() } catch [Exception] { # Ignore errors if unable to close the browser }
您可能会通过适当的
CreateRunspace
调用引入一个单独的脚本,并开发Synchronized
对象以允许从连接到主脚本的单独 PowerShell 运行空间控制$driver.GetScreenshot
调用的调用(这目前正在进行中),方式类似于先前示例中控制的系统托盘通知图标。脚本的 Selenium RC 版本将加载不同的库并切换到
Nunit
库Asserts
。$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')
脚本的其余部分将保持不变。
当然,您可以在 PowerShell ISE 中直接创建脚本,这将节省大量开发时间。
要使用最新版本的 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 的类,并使用
EnumWindows
、GetWindowInfo
、EnumPropsEx
、GetProp
、GetWindowText
、GetWindowTextLength
、GetWindowThreadProcessId
win32 API 从user32.dll
通过[DllImport()]
并加载 Windows.h 中定义的许多必需结构来访问窗口句柄并调用PostMessage
或SendMessage
到所需的按钮,或者简单地调用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; }
请注意,如果不发送“Enter”键,Windows Explorer 将忽略后台输入的文本,并以原始位置/名称保存文件。
修改后的代码包含在归档文件中。只需进行最少的努力,即可将类集成到 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." }
结果将可供调用脚本使用……
当业务运行混合 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 }
该脚本可以在除 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 上翻译文本。页面包含以下片段。
<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)
然后等待以下元素出现。
<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 语法并进行其他必要的调整。创建格式化程序所需的所有内容只有一个文件。
需要小心的一点是不要从基于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>'+ // ...
在开发后期,您将安排源代码以适合 xpi,并创建
chrome.manifest
、install.rdf
和format-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
要使用格式化程序:
- 打开 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, '')
显然,这里的 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 }
此按钮用于显示调试消息和(进行中)暂停脚本的执行。
$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
完整源代码可在 zip 文件中找到。
Selenium EventFiring WebDriver 示例
以下是
Selenium
EventFiringWebDriver
从 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)
杂项。实用程序
您可以将 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()
函数操作来自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) }
在 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) }
极端情况
为了举例说明 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 { ...
在此示例中,可能应该避免转换。完整的脚本源代码可在源 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
cmdletTypeDefinition
的文本参数中,并确保其可运行。. ./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 所需的单个方法——带窗口句柄的构造函数。上面代码中的其他属性
Data
和Message
不是接口所必需的,但对于将各部分连接在一起至关重要。 -
最后,修改代码以处理调用者。
- 将参数传递给
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
并确认它仍然运行。运行脚本会产生相同的对象,该对象可供脚本和窗体使用。
- 将参数传递给
实际转换为 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()
将
$caller
和showDialog(...)
移到 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'))"})
并进行了点击。结果符合预期。
总结一下,根据 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 )
它根据
ConsoleHost
与Windows PowerShell ISE Host
中的*主机*功能呈现不同的效果。并返回所选选项的索引 - 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 配置。后者非常不用户友好。
在后台,所有信息都存储在一个注册表项中。这使得从远程主机加载这些信息成为自动化的良好候选,但操作员的角色仍然至关重要,因为环境景观之间存在细微差别:哪些 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
Treeview
s - 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 示例和下载。