从印度小哥那里受到启发,PowerShell不止可以做批处理,自动化。还能做界面做GUI,也能实现很好的交互。因为PowerShell是可以和.Net 程序无缝集成的。所以.Net能用的东西在PowerShell里也几乎都能使用。那么如果想让PowerShell的交互性更强,更友好无疑是创建一个友好GUI界面了。本文介绍如何在PowerShell中创建窗体程序。
目录
前言
一、创建Hello World窗口
1. 手残篇
2. 使用Assembly Name引入.net 库
3.使用DLL文件路径引入.net 库
4. 再次使用Assembly Name引入.net 库
二、添加Hello Button控件
1. 添加Button
2. 添加事件
3. 防止窗口假死
4. 控制按钮的状态,拒绝加班
三、添加进度条
1. 添加进度条和状态标签
2. 添加更新状态的方法
总结
这个前xi有点长,啊不对是前言。请耐心看完。
我为什么会写这篇文章呢?主要还是被印度小哥灵活的头脑震惊到了。
事情是这样的,之前在某外资银行工作的时候,因为大部分的系统和服务器都是运行在Windows下的。需要不定期在服务器上安装微软的补丁,这是一个纯手工的工程~~~OMG囧囧。你没看错,即使今天也仍然有很多企业在手动干这件事情。
那么装的多了难免有点腻歪,于是印度小哥成立了一个自动化部门,他们发布了一个宏大的愿景,就是立志要提高公司的自动化水平。(至于提高多少他们没说)
所以他们的第一个目标就瞅准了给Windows打补丁这回事,按理说计算机应该可以自动安装补丁才对啊?我们自己的机器不都是自动安装的吗?
事情没有那么简单。
由于银行对安全和风险控制及其严格,所以一般来讲银行的服务器是不能连外网的。当然也有其它的技术在内网实现补丁管理这一过程。但是,这不还没实施吗!而银行每一台服务器上都有可能跑着不同的服务和应用,某些补丁和更新动不动就重启服务器或者服务啥的,而IT部门是不知道的,因此打补丁这个看似简单的过程变得复杂起来。
刚开始IT部门尝试过统一打补丁什么的,但是每个服务器运行的应用程序不同,对可用性的要求不同。众口难调,补丁部门又不想承担过多的责任。比如打补丁把数据弄丢啦,把服务干倒了,银行系统宕机什么的。所以最后为了图省事就干脆把这个权限下放给对应的应用维护团队了,因为每个团队对自己的应用是最清楚的。(有点要向DevOps转向的意思哈^_^)
而这时候自动化部门出动了,豪言要自动化这一过程。于是他们开发了一套程序给每个应用团队让他们使用这个程序来自动化打包流程。
而我们部门刚刚得到这个程序的时候,一看是几个PowerShell脚本。那就先运行一下试试看喽。一运行,我艹,什么玩意不是跑个脚本就把补丁打完了吗?怎么还出来WinForm界面了。不简单~不简单~
于是关了脚本,用VSCode打开他们的脚步仔细的拜读了一下。不读不知道,一读真奇妙。明明可以用C#来直接写个Winform, 他不,非要用PowerShell来写。明明PowerShell脚本加几个参数就能一键运行打补丁,他不,非要在PowerShell里面用WinForm创建一个交互式的界面,里面又是添加补丁路径,又是拷贝补丁包,还用WinForm做了Log和进度显示。自动化团队还真是下了一番功夫。
之前在 PowerShell: 如何使用PowerShell操作FTP? 中也提到PowerShell也是可以和.net 代码无缝集成的,知道归知道,但是真的这么用的还是第一次见,又给自己涨姿势了。毕竟一般情况下我会认为PowerShell就是用来跑些后台命令或者脚本的,如果有需要比较复杂的交互就直接用VS写了。但是用PowerShell来写这种程序也有些好处,那就是修改维护比较简单-即改即用,无需编译。而且PowerShell里面对有些事件和数据的处理也比较简单。
因此对PowerShell中使用WinForm程序做了一个简单的总结,以备后用。
虽然只是个Hello World,但是也经历了几番曲折。
上来想都没想,直接上代码
$Form = New-Object System.Windows.Forms.Form
$Form.Name = "Form1";
$Form.Text = "PowerShell Form (Hello world!)";
$Form.ShowDialog()
古人曰:欲速则不达。果然,直接抛错了:
一看很明显这不就是对应Type找不到嘛,应该是对应的.net 库没有加载。
记得PowerShell里面可以用using关键字直接引入DLL,那我也来高级一把。
using assembly "System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
然鹅,事情并没有沿着我的脑回路发展
使用Assembly Name~扑街
难道using 不能直接从GAC里面加载DLL ?查看了一下微软官方示例,我去难道要用路径才行么?
找到这个DLL对应的文件路径再试一次
using assembly "C:\WINDOWS\Microsoft.NET\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll"
bingo~~~ 没有抛错没有异常提示,搞定!
事情上面第三步似乎也就完了,但是我偏不信邪。用Assembly Name看起来多友好啊,不用关心这个DLL的具体路径,就可以自动找到对应的DLL。但是using只支持文件路径这一招,没法破。
那么问题的本质是什么呢?-- 当前的运行程序没有加载对应的DLL导致相关的类型找不到。
既然是这样那用反射的方法是不是也可以?只要DLL加载到当前的运行环境就可以了。重要的是使用反射的方式可以使用Assembly Name。
[System.Reflection.Assembly]::Load("System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
程序完美运行,nice~~~
但是上面红框框里面是什么东西?哦哦,原来是[System.Reflection.Assembly]::Load 方法是由返回值的如果我们不做处理的话PowerShell会默认输出到控制台窗口中。往往我们使用一个临时变量来接住它就可以让他不输出到窗口中。比如这样:
$ass=[System.Reflection.Assembly]::Load("System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
但是我不想要这样,鬼知道我在后面会不会莫名其妙的使用到这个变量。所以既然不想要这个返回值,就把它丢弃掉吧,虽然有点残忍。就像下面这样:
[System.Reflection.Assembly]::Load("System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") | Out-Null
我们可以使用管道符把没用的返回值或者输出,扔到Out-Null中,这样当脚本跑起来的时候就是我们想要的内容了。
这应该是Form的ShowDialog方法的返回值。把运行窗体的方式按照Winform里面的运行方式稍加修改。
[System.Reflection.Assembly]::Load("System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") | Out-Null
$Form = New-Object System.Windows.Forms.Form
$Form.Name = "Form1";
$Form.Text = "PowerShell Form (Hello world!)";
[System.Windows.Forms.Application]::SetHighDpiMode('SystemAware') | Out-Null;
[System.Windows.Forms.Application]::EnableVisualStyles() | Out-Null;
[System.Windows.Forms.Application]::Run($Form) | Out-Null;
这样就更像一个Winform程序了。再为Form设置更多属性,比如窗口大小等。。。
$Form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font;
$Form.ClientSize = New-Object System.Drawing.Size(800, 450);
$Form.Name = "Form1";
$Form.Text = "PowerShell Form (Hello world!)";
既然Hello World 已跑通,那么接下来就看看如何添加其它控件吧。
脑子里面第一个跳出来的就是Button控件,所以就来添加一个Hello Button吧。好久没有写Winform程序好多用法都忘记了。没关系,用VS新建一个.net 的winform程序参考参考~嘿嘿。
按照Winform程序的方式:
在PowerShell中创建button对象并为其设置属性:
# Create a new button instance
$button1 = New-Object System.Windows.Forms.Button;
# Set button property
$button1.Location = New-Object System.Drawing.Point(257, 150);
$button1.Name = "button1";
$button1.Size = New-Object System.Drawing.Size(284, 103);
$button1.TabIndex = 0;
$button1.Text = "工作使我快乐";
$button1.UseVisualStyleBackColor = $true;
然后再把Button控件添加到Form窗口中。
$Form.Controls.Add($button1);
漂亮,button加好了,那么接下来就要干活了,因为“工作使我快乐”。
按照Winform里面的流程给Button添加一个点击事件就能干活了
那么直接转成PowerShell代码:
$button1.Click+=New-Object System.EventHandler({
param($arg1,$arg2)
Write-Host "我开工作了。。。"
})
然而并没有是我快乐,因为它出错了。
大概意思就是找不到对应的重载方法啥的blabla~~
那我就跟暴力一点,反正需要的是个委托,那么在PowerShell里面传个代码片段就是可以的。
$button1.Click+={
param($arg1,$arg2)
Write-Host "我开工作了。。。"
}
但是,但是,它居然说找不到Click这个属性。
记不记得有一种从天而降的掌法。。。不对是曾经用反射的时候类的事件添加和删除其实用的是两个方法 Add_XXX, Remove_XXX。既然这样那就懂了。
$button1.Add_Click({
param($arg1,$arg2)
Write-Host "我开工作了。。。"
})
然后,哇啦,我开始工作了。。。
我开心的像得到了糖果的小屁孩。。。
除此之外还可以参考Register-ObjectEvent来注册事件。
我是一个很爱工作的人,有时候工作起来就没完,也可能加班到很晚。所以点这个button的时候可能会让窗口卡死,因为太忙了。
先来100份工作热热身,修改代码如下。
$button1.Add_Click({
param($arg1, $arg2)
Write-Host "我开工作了。。。"
$count = 1;
$totalJob = 100
do {
Write-Host "完成第 $count 份工作"
Start-Sleep -Milliseconds 100
$count++
} while ($count -le $totalJob)
Write-Host "工作做完,开始浪了。。。"
})
Winform里面本身有很多种方法来解决这个问题。其中之一便是使用另外开一个线程。并不是其它方法不能尝试,而是看过其它方案之后直觉开线程这条路可能更容易一点。
使用Start-Job来多开一个线程来执行耗时操作。
$button1.Add_Click({
param($arg1, $arg2)
Start-Job -ScriptBlock {
Write-Host "我开工作了。。。"
$count = 1;
$totalJob = 100
do {
Write-Host "完成第 $count 份工作"
Start-Sleep -Milliseconds 100
$count++
} while ($count -le $totalJob)
Write-Host "工作做完,开始浪了。。。"
}
})
保存执行,咦,界面倒是没卡了,但是结果却没了。
查看了一下微软官方文档,Start-Job起的相当是一个后台job,它的结果不会直接输出到控制台上。必须使用Receive-Job来接收结果。
$button1.Add_Click({
param($arg1, $arg2)
Start-Job -ScriptBlock {
Write-Host "我开工作了。。。"
$count = 1;
$totalJob = 100
do {
Write-Host "完成第 $count 份工作"
Start-Sleep -Milliseconds 100
$count++
} while ($count -le $totalJob)
Write-Host "工作做完,开始浪了。。。"
} | Receive-Job
})
但是结果仍然没有出来。我猜原因可能是后台job还没起来,所以啥结果也没有接收到。那就等待一下试试呢?
$button1.Add_Click({
param($arg1, $arg2)
Start-Job -ScriptBlock {
Write-Host "我开工作了。。。"
$count = 1;
$totalJob = 100
do {
Write-Host "完成第 $count 份工作"
Start-Sleep -Milliseconds 100
$count++
} while ($count -le $totalJob)
Write-Host "工作做完,开始浪了。。。"
} | Receive-Job -Wait -AutoRemoveJob
})
这回结果是出来了,但是很不幸界面又卡住了。因为在Click事件里面,虽然起来后台job去完成工作,但是为了等这个结果,等的花都谢了。于是又Google了一番,发现除了Start-Job还有个Start-ThreadJob而这个命令按官方解释是一个更加轻量级的后台job,关键是它有个参数-StreamingHost看起来就是可以直接输出结果的那种。先来试试:
$button1.Add_Click({
param($arg1, $arg2)
Start-ThreadJob -ScriptBlock {
Write-Host "我开工作了。。。"
$count = 1;
$totalJob = 100
do {
Write-Host "完成第 $count 份工作"
Start-Sleep -Milliseconds 100
$count++
} while ($count -le $totalJob)
Write-Host "工作做完,开始浪了。。。"
} -StreamingHost $Host
})
好家伙,界面不卡了,内容也出来了,但是老板不断给我派活,停都停不下来了。《my heart will not go on》了。
为了防止老板一直给我派活,那么在开始干活之前我得把门关上不让老板继续给我派活。有多少活都得等着,等我缓过劲来的。比如这样:
$button1.Add_Click({
# 干活之前开启免打扰模式
$button1.Enabled=$false;
Start-ThreadJob -ScriptBlock {
Write-Host "我开工作了。。。"
$count = 1;
$totalJob = 100
do {
Write-Host "完成第 $count 份工作"
Start-Sleep -Milliseconds 100
$count++
} while ($count -le $totalJob)
Write-Host "工作做完,开始浪了。。。"
# 干完活之后继续接单
$button1.Enabled=$true
} -StreamingHost $Host
})
但是事情又出茬子了,虽然免打扰设置成功,活也干完了,但是门却被老板锁死了。这下抢菜都抢不到了估计会被饿死在办公室了。
分析了一下大概是因为他们处在不同的进程中无法共享资源。那我能不能通过钥匙孔大喊一声:老板我干完活了,可以接单了。
$button1.Add_Click({
# 干活之前开启免打扰模式
$button1.Enabled=$false;
Start-ThreadJob -ScriptBlock {
param($Callback)
Write-Host "我开工作了。。。"
$count = 1;
$totalJob = 100
do {
Write-Host "完成第 $count 份工作"
Start-Sleep -Milliseconds 100
$count++
} while ($count -le $totalJob)
Write-Host "工作做完,开始浪了。。。"
# 干完活之后通知主线程继续接活。
Invoke-Command -ScriptBlock $Callback -ArgumentList $true
} -StreamingHost $Host -ArgumentList {
param($Enable)
#给后台job传递一个callback方法,在工作结束之后修改button状态
$Form.BeginInvoke(
{
$button1.Enabled=$Enable
}
)
}
})
然后果然,活可以一单接一单的干了。我开心的像个小孩纸囧囧。
通过第二部分几乎解决了Winform中能遇到的绝大部分问题。那么接下来我们可以把界面做的更炫酷一点了。比如说至少添加个进度条,能够更直观的展示我的工作进度。要不我费那么大劲弄界面干啥?
添加两个新控件,一个Label一个Progressbar,一个用来显示状态,一个用来显示进度:
#
# progressBar1
#
$progressBar1 = New-Object System.Windows.Forms.ProgressBar;
$progressBar1.Location = New-Object System.Drawing.Point(179, 409);
$progressBar1.Name = "progressBar1";
$progressBar1.Size = New-Object System.Drawing.Size(609, 29);
$progressBar1.TabIndex = 1;
#
# label1
#
$label1 = New-Object System.Windows.Forms.Label;
$label1.AutoSize = $true;
$label1.Location = New-Object System.Drawing.Point(12, 418);
$label1.Name = "label1";
$label1.Size = New-Object System.Drawing.Size(50, 20);
$label1.TabIndex = 2;
$label1.Text = "我是状态";
然后再把他们分别添加到主窗体中:
$Form.Controls.Add($label1);
$Form.Controls.Add($progressBar1);
如下所示:
到目前为止状态标签和进度条都没有办法显示我的工作状态的。我们需要像之前处理button的状态一样写个代理方法传给后台job。顺便把之前的开门关门的代码也给单独拿出来。
$OpenTheDoor = {
param($Enable)
#给后台job传递一个callback方法,在工作结束之后修改button状态
$Form.Invoke(
{
$button1.Enabled = $Enable
}
)
}
$ReportProgress = {
param($Progress, $Activity, $Status)
$Form.Invoke({
$label1.Text = "$Activity $Status"
$progressBar1.Value = $Progress
})
}
修改Button的Click 方法
$button1.Add_Click({
# 干活之前开启免打扰模式
$button1.Enabled = $false;
Start-ThreadJob -ScriptBlock {
param($Callback,$ProgressReporter)
Write-Host "我开工作了。。。"
$count = 1;
$totalJob = 100
do {
Write-Host "完成第 $count 份工作"
# 通知老板我的工作进度
Invoke-Command -ScriptBlock $ProgressReporter -ArgumentList $count,"努力工作",$count
Start-Sleep -Milliseconds 100
$count++
} while ($count -le $totalJob)
Write-Host "工作做完,开始浪了。。。"
# 通知老板我的工作已经完成100%
Invoke-Command -ScriptBlock $ProgressReporter -ArgumentList 100,"工作完成^_^",100
# 干完活之后通知主线程继续接活。
Invoke-Command -ScriptBlock $Callback -ArgumentList $true
} -StreamingHost $Host -ArgumentList $OpenTheDoor, $ReportProgress
})
保存,运行:
到这里PowerShell中winform的实验就算告一段落了,累死宝宝了~~(一个大龄宝宝)。
其它控件的使用和操作都是类似,当然我不可能分享印度小哥之前写的程序了,公司也不允许啊。
实验的完整代码如下,读者朋友们可以在此基础上扩展自己的应用。
[System.Reflection.Assembly]::Load("System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") | Out-Null
$Form = New-Object System.Windows.Forms.Form
$progressBar1 = New-Object System.Windows.Forms.ProgressBar;
$button1 = New-Object System.Windows.Forms.Button;
$label1 = New-Object System.Windows.Forms.Label;
#
# progressBar1
#
$progressBar1.Location = New-Object System.Drawing.Point(179, 409);
$progressBar1.Name = "progressBar1";
$progressBar1.Size = New-Object System.Drawing.Size(609, 29);
$progressBar1.TabIndex = 1;
#
# label1
#
$label1.AutoSize = $true;
$label1.Location = New-Object System.Drawing.Point(12, 418);
$label1.Name = "label1";
$label1.Size = New-Object System.Drawing.Size(50, 20);
$label1.TabIndex = 2;
$label1.Text = "我是状态";
$OpenTheDoor = {
param($Enable)
#给后台job传递一个callback方法,在工作结束之后修改button状态
$Form.Invoke(
{
$button1.Enabled = $Enable
}
)
}
$ReportProgress = {
param($Progress, $Activity, $Status)
$Form.Invoke({
$label1.Text = "$Activity $Status"
$progressBar1.Value = $Progress
})
}
$button1.Location = New-Object System.Drawing.Point(257, 150);
$button1.Name = "button1";
$button1.Size = New-Object System.Drawing.Size(284, 103);
$button1.TabIndex = 0;
$button1.Text = "工作使我快乐";
$button1.UseVisualStyleBackColor = $true;
$button1.Add_Click({
# 干活之前开启免打扰模式
$button1.Enabled = $false;
Start-ThreadJob -ScriptBlock {
param($Callback,$ProgressReporter)
Write-Host "我开工作了。。。"
$count = 1;
$totalJob = 100
do {
Write-Host "完成第 $count 份工作"
# 通知老板我的工作进度
Invoke-Command -ScriptBlock $ProgressReporter -ArgumentList $count,"努力工作",$count
Start-Sleep -Milliseconds 100
$count++
} while ($count -le $totalJob)
Write-Host "工作做完,开始浪了。。。"
# 通知老板我的工作已经完成100%
Invoke-Command -ScriptBlock $ProgressReporter -ArgumentList 100,"工作完成^_^",100
# 干完活之后通知主线程继续接活。
Invoke-Command -ScriptBlock $Callback -ArgumentList $true
} -StreamingHost $Host -ArgumentList $OpenTheDoor, $ReportProgress
})
$Form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font;
$Form.ClientSize = New-Object System.Drawing.Size(800, 450);
$Form.Name = "Form1";
$Form.Text = "PowerShell Form (Hello world!)";
$Form.Controls.Add($label1);
$Form.Controls.Add($progressBar1);
$Form.Controls.Add($button1);
# $Form.ResumeLayout($false);
# $Form.ShowDialog()
[System.Windows.Forms.Application]::SetHighDpiMode('SystemAware') | Out-Null;
[System.Windows.Forms.Application]::EnableVisualStyles() | Out-Null;
[System.Windows.Forms.Application]::Run($Form) | Out-Null;
以上就是对PowerShell中使用Winform的一个总结。以及针对Winform中的几个比较常见的问题尝试在PowerShell环境下进行了解决。比如假死问题,异步计算问题,传参数的问题,控制台输出问题,DLL加载问题以以及如何添加事件等。当然这些过程中还会存在很多未知的问题,留给读者去发掘,并积极分享,谢谢!