My script stops when I´m calling the message box or the voice output.
The script is waiting to complete the task what is the normal behavior in PowerShell.
But how it is possible to execute this script without a break at the voice output?
I want to execute this code without a break:
[System.Windows.Forms.MessageBox]::Show("stop")
$voice = new-object -com SAPI.SpVoice
$voice.Speak("Hello Stackoverflow!")
[System.Windows.Forms.MessageBox]::Show("done")
Like (Not working):
[System.Windows.Forms.MessageBox]::Show("stop")
$job = start-job {
$voice = new-object -com SAPI.SpVoice
$voice.Speak($text)
}
[System.Windows.Forms.MessageBox]::Show("done")
or like (Also not working):
$test = "Hello"
[System.Windows.Forms.MessageBox]::Show("stop")
$backPS = [powershell]::create()
[void] $backPS.AddScript("$voice = new-object -com SAPI.SpVoice
$voice.Speak($test)")
[System.Windows.Forms.MessageBox]::Show("done")
The background job approach works for me:
$job = start-job { (new-object -com SAPI.spVoice).Speak("hi") }
$text needs to either be passed to the background job or defined in the background job script block.
$job = start-job { (new-object -com SAPI.spVoice).Speak($args[0]) } -arg "hi"
The other blocking code is [System.Windows.Forms.MessageBox]::Show. Do you want this to be synchronous or asynchronous?
Related
EDIT : I was able to get it working, see below for my solution. The commenters below are correct that Powershell isn't really ideal for GUI's and threading, but it can be done.
I've got a form in Powershell that uses Start-Job to run functions in the background without freezing the GUI. My goal is to continuously check the status of those jobs for their output. I managed to use the Windows Forms Timer to check the results of the job and update the GUI accordingly.
It's all working fine, but it seems sloppy. Is this the best way to accomplish a GUI refresh? I'm relatively new to Powershell and I want to improve my coding.
Example of what I'm doing:
$jobScript = {
Start-Sleep 5
Write-Output "The job has finished running"
}
$timerScript = {
$timer.Stop()
$jobResult = Get-Job | Receive-Job -Keep
if ($jobResult) {
$btn.text = $jobResult
} else {
$timer.Start()
}
}
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ClientSize = '300,300'
$form.topmost = $true
$btn = New-Object System.Windows.Forms.Button
$btn.Text = "The job is still running"
$btn.Width = 300
$btn.Height = 300
$form.Controls.Add($btn)
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 100
$timer.add_Tick($timerScript)
$timer.Start()
Start-Job -ScriptBlock $jobScript
$form.ShowDialog()
Update: My solution
Using Register-ObjectEvent did not work, it seemed like it was fighting with the GUI for the thread. Instead I was able to use [System.Windows.Forms.Application]::DoEvents(). This allows the GUI to be moved around, and once it's done being moved, the thread will resume. The big caveat here is that execution is paused as long as the GUI is being moved, so if your code needs to react to the background job on a time limit, this could cause errors.
Example code block:
$jobScript =
{
Start-Sleep 5
Write-Output "The job is completed"
}
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ClientSize = '300,300'
$form.topmost = $true
$btn = New-Object System.Windows.Forms.Button
$btn.Text = "..."
$btn.Width = 300
$btn.Height = 300
$form.Controls.Add($btn)
$btn.add_click({
$btn.Text = "Starting job"
$jobby = Start-Job -ScriptBlock $jobScript
Do {[System.Windows.Forms.Application]::DoEvents()} Until ($jobby.State -eq "Completed")
$btn.Text = Get-Job | Receive-Job
})
$form.ShowDialog()
You can use events:
$job = Start-Job {sleep 3; Write-Output "Dummy job completed"}
$callback = {
Write-Host "Event Fired"
Unregister-Event -SourceIdentifier "DummyJob"
Write-Host ($job | Receive-Job)
}
$evt = Register-ObjectEvent -InputObject $job -EventName StateChanged -SourceIdentifier "DummyJob" -Action $callback
# to remove all
Get-Job | Remove-Job -Force # jobs and events
Get-EventSubscriber | Unregister-Event # events
You might want to post this on CodeReview.StackExchange.com.
I sort of hate it when people build UI's in Powershell. If you want a proper Windows forms app, just write it in C#. So I disagree with the design at its premise.
I like your impulse to move away from the polling design; you initiate the job then poll to see if it's completed. I think an event handler might be a better choice. Check out the section "Monitor a Background Job" in the article PowerShell and Events: Object Events. It's an oldie but a goodie.
I've been working on a simple proof of concept / template of a script in which I have the default Runspace to run heavy tasks and another one to run a responsive GUI.
I have tested various methods to be able to communicate the runspaces. At first I tried the Control.Invoke but some opinions on different forums and a weird behaviour on tests led me to use a message based intercomunication based on a Synchronized HashTable. ProgressBar works with control.Invoke, but, executing some other actions, like disabling multiple buttons on a form performs very slow.
1st problem: I would like to show a progressbar (marquee) when the task is executing, changing its visible state. However, the progressbar is showed when the script runs on ISE, but not when it does on console. I think it is because main runspace is busy, but I don't understand how it could affect the GUI runspace...
2nd: on the script I post below, I'm using scriptblocks through a stack variable to pass the commands between runspaces. Then, each runspace (main runspace does it through pooling and GUI through a timer) checks if a task is pending of execution in the stack, and, if so, executes it. However if I would want to call a function declared on the other runspace (Test-OutOfMainScopeUiFunction in this example), I couldn't. I would get a runtime error on the scriptblock. I have thought solutions to this like:
-Importing functions on both runspaces
-Making functions global or use functon delegates¿?
-Passing a string with the commands to execute in spite of a scriptblock -> Using this one at the moment, but don't like very much... error prone
Any solution to the progress bar problem or script improvements will be appreciated. Thanks!
$global:x = [Hashtable]::Synchronized(#{})
$global:x.MainDispatcher = new-object system.collections.stack
$global:x.UiDispatcher = new-object system.collections.stack
$global:x.GuiExited = $false
$formSB = {
[reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
[reflection.assembly]::LoadWithPartialName("System.Drawing")
Function Test-OutOfMainScopeUiFunction
{
$x.Host.Ui.WriteLine("Test-OutOfMainScopeUiFunction Executed")
}
Function Execute-OnMainRs
{
param(
[Parameter(Mandatory=$true)]
[ScriptBlock]$ScriptBlock
)
$x.Host.Ui.WriteLine("`r`nAdding task")
$x.MainDispatcher.Push($ScriptBlock)
$x.Host.Ui.WriteLine("Task added")
$x.Host.Ui.WriteLine("Task: $($x.MainDispatcher)")
}
$form = New-Object System.Windows.Forms.Form
$button = New-Object System.Windows.Forms.Button
$button.Text = 'click'
$button.Dock = [System.Windows.Forms.DockStyle]::Right
$progressBar = (New-Object -TypeName System.Windows.Forms.ProgressBar)
$ProgressBar.Style = [System.Windows.Forms.ProgressBarStyle]::Marquee
$ProgressBar.MarqueeAnimationSpeed = 50
$ProgressBar.Dock = [System.Windows.Forms.DockStyle]::Bottom
$ProgressBar.Visible = $false
$label = New-Object System.Windows.Forms.Label
$label.Text = 'ready'
$label.Dock = [System.Windows.Forms.DockStyle]::Top
$timer=New-Object System.Windows.Forms.Timer
$timer.Interval=100
$timer.add_Tick({
if($x.UiDispatcher.Count -gt 0)
{
& $($x.UiDispatcher.Pop())
}
})
$form.Controls.add($label)
$form.Controls.add($button)
$form.Controls.add($progressBar)
Add-Member -InputObject $form -Name Label -Value $label -MemberType NoteProperty
Add-Member -InputObject $form -Name ProgressBar -Value $progressBar -MemberType NoteProperty
$button.add_click({
Execute-OnMainRs -ScriptBlock {
write-host "MainRS: Long Task pushed from the UI started on: $(Get-Date)"
start-sleep -s 2
write-host "MainRS: Long Task pushed from the UI finished on: $(Get-Date)"
}
})
$form.add_closed({ $x.GuiExited = $true })
$x.Form = $form
$timer.Start()
$form.ShowDialog()
}
Function Execute-OnRs
{
param(
[Parameter(Mandatory=$true)]
[ScriptBlock]$ScriptBlock
)
$x.Host = $Host
$rs = [RunspaceFactory]::CreateRunspace()
$rs.ApartmentState,$rs.ThreadOptions = "STA","ReUseThread"
$rs.Open()
$rs.SessionStateProxy.SetVariable("x",$x)
$ps = [PowerShell]::Create().AddScript($ScriptBlock)
$ps.Runspace = $rs
$handle = $ps.BeginInvoke()
#Almacenar variables del RS
$x.Handle = $handle
$x.Ps = $ps
}
Function Execute-OnUiRs
{
param(
[Parameter(Mandatory=$true)]
[ScriptBlock]$ScriptBlock
)
$x.UiDispatcher.Push($ScriptBlock)
}
Function Dispatch-PendingJobs
{
while($global:x.GuiExited -eq $false) {
if($global:x.MainDispatcher.Count -gt 0)
{
Execute-OnUiRs -ScriptBlock {
$msg = "UIRS: MainThread informs: Long Task started on $(Get-Date)."
$global:x.Form.Label.Text = $msg
$global:x.Form.ProgressBar.Visible = $true
$x.host.ui.WriteLine($msg)
#Next line throws an error visible on UI runspace error stream
Test-OutOfMainScopeUiFunction
}
& $($global:x.MainDispatcher.Pop())
Execute-OnUiRs -ScriptBlock {
$msg = "UIRS: MainThread informs: Long Task finished on $(Get-Date)."
$global:x.Form.Label.Text = $msg
$global:x.Form.ProgressBar.Visible = $false
$x.host.ui.WriteLine($msg)
}
write-host "UI Streams: $($global:x.Ps.Streams |out-string)"
}
else
{
start-sleep -m 100
}
}
}
Found the solution... http://community.idera.com/powershell/powertips/b/tips/posts/enabling-visual-styles
VisualStyles must be enabled first. The problem is not related with runspaces stuff. This is a brief and clearer code example taken from Power Shell marquee progress bar not working with the fix.
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$window = New-Object Windows.Forms.Form
$window.Size = New-Object Drawing.Size #(400,75)
$window.StartPosition = "CenterScreen"
$window.Font = New-Object System.Drawing.Font("Calibri",11,[System.Drawing.FontStyle]::Bold)
$window.Text = "STARTING UP"
$ProgressBar1 = New-Object System.Windows.Forms.ProgressBar
$ProgressBar1.Location = New-Object System.Drawing.Point(10, 10)
$ProgressBar1.Size = New-Object System.Drawing.Size(365, 20)
$ProgressBar1.Style = "Marquee"
$ProgressBar1.MarqueeAnimationSpeed = 20
$window.Controls.Add($ProgressBar1)
$window.ShowDialog()
I would like to create a GUI that shows a loading bar while a job is running in the background. For simplicity, I've made the job an infinite loop so it should always be running. I've only included necessary parts of the code:
$Label = new-object system.windows.forms.Label
$Label.Font = 'Ariel,12pt'
$Label.Text = ""
$Label.AutoSize = $True
$Label.Location = new-object system.drawing.size(50,10)
$Form.Controls.Add($Label)
$LoadingAnimation = #(".....","0....",".0...","..0..","...0.","....0",".....")
$AnimationCount = 0
$test = start-job -Name Job -ScriptBlock { for($t=1;$t -gt 0; $t++){} }
while ($test.JobStateInfo.State -eq "Running")
{
$Label.Text = $LoadingAnimation[($AnimationCount)]
$AnimationCount++
if ($AnimationCount -eq $LoadingAnimation.Count){$AnimationCount = 0}
Start-Sleep -Milliseconds 200
}
Upon testing this code in the console, just using Write-Host instead of $Label.Text, it works just fine. What needs to be done differently to get this to work in a windows form created by PowerShell?
In PowerShell, you can create all sorts of status, including multi-level, using Write-Progress. Don't forget to call with -Completed when done (a common mistake I see).
Get-Help Write-Progress
After going through the little details of the script, I found the problem. This is how I activated my form:
$Form.Add_Shown({$Form.Activate()})
[void] $Form.ShowDialog()
This caused the script to stop when the form was launched, ShowDialog stops the script to allow interaction. The fix was:
$Form.Add_Shown({$Form.Activate()})
[void] $Form.Show()
Using Form.Show lets the script to continue to run because it doesn't require interaction.
I have a function inside a Job which takes a Screenshot. The Script itself gets executed by Taskscheduler as a domain admin. I also checked "Run with highest privileges".
The Job should do a Screenshot and then send an E-Mail, but both of those thing don't happen. I also don't see an error message (maybe because it's a background job?). Maybe the E-Mail doesn't get sent because it wants to attach the screenshot to the e-mail, but can't because the screenshot wasn't created.
Why does my function not Take the Screenshot? The domain admin has privileges to write to the destination. The screenshot gets created when I run the function outside the start-job. if i have the function inside start-job it doesn't get created, doesn't matter if the script is started via Taskscheduler or manually.
What am I missing?
The following Code starts the Job and takes the screenshot:
Start-Job -Name LogikWebserverWatch {
function Take-Screenshot([string]$outfile)
{
[int]$PrtScrnWidth = (gwmi Win32_VideoController).CurrentHorizontalResolution
[int]$PrtScrnHeight = (gwmi Win32_VideoController).CurrentVerticalResolution
$bounds = [Drawing.Rectangle]::FromLTRB(0, 0, $PrtScrnWidth, $PrtScrnHeight)
$bmp = New-Object Drawing.Bitmap $bounds.width, $bounds.height
$graphics = [Drawing.Graphics]::FromImage($bmp)
$graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size)
$bmp.Save($outfile)
$graphics.Dispose()
$bmp.Dispose()
}
while ((Get-Process LogikWebserver).Responding) {sleep -Milliseconds 50}
if (!(Get-Process LogikWebserver).Responding) {
Try{
$utf8 = New-Object System.Text.utf8encoding
$datetime = (get-date).ToString('yyyyMMdd-HHmmss')
Take-Screenshot -outfile C:\Install\LogikWebserverErrorReporting\Screenshot-$datetime.png
# some more code [...]
} Catch { some more code [...] }
}}
The documentations says that:
A Windows PowerShell background job runs a command without interacting with the current session.
So you may have to load the required assemblies inside your job before it will work.
When I tried your code above, it only created a screenshot when run outside of a job (as you mentioned), however adding this line to the top of the Start-Job ScriptBlock caused it to work from inside the job as well:
[Reflection.Assembly]::LoadWithPartialName("System.Drawing")
Or, as the above is now depracated:
[System.Reflection.Assembly]::LoadFrom("C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\System.Drawing.dll")
Or
Add-Type "C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\System.Drawing.dll"
Note that I have not tested this when running from a schedued task.
dont use function . just write your function code in script block like this :
Start-Job -Name LogikWebserverWatch {
Add-Type -AssemblyName "System.Drawing"
[int]$PrtScrnWidth = (gwmi Win32_VideoController).CurrentHorizontalResolution
[int]$PrtScrnHeight = (gwmi Win32_VideoController).CurrentVerticalResolution
$bounds = [Drawing.Rectangle]::FromLTRB(0, 0, $PrtScrnWidth, $PrtScrnHeight)
$bmp = New-Object Drawing.Bitmap $bounds.width, $bounds.height
$graphics = [Drawing.Graphics]::FromImage($bmp)
$graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size)
$bmp.Save($outfile)
$graphics.Dispose()
$bmp.Dispose()
}
while ((Get-Process LogikWebserver).Responding) {sleep -Milliseconds 50}
if (!(Get-Process LogikWebserver).Responding) {
Try{
$utf8 = New-Object System.Text.utf8encoding
$datetime = (get-date).ToString('yyyyMMdd-HHmmss')
Take-Screenshot -outfile C:\Install\LogikWebserverErrorReporting\Screenshot-$datetime.png
# some more code [...]
} Catch { some more code [...] }
}
OK so I am newer to using windows forms inside Powershell, and I am having a bit of trouble with the form being all laggy since there is a continuous ping running. Fundamentally all I need to do is display the IP address each time it runs a ping. I tried to search around and it seems like the proper way to do this is with a running job in the background?
I think the way this is currently written that the ping happens in the background but I'm not sure how to update the form (or if its even possible to make it visible to it?)
Here is a sample, any guidance with this would be greatly appreciated.
function global:ContinuousPing{
$global:job = start-job {
while($true){
$pingStatus = Test-Connection google.com -Count 1
$label.text = $pingStatus.IPV4Address.IPAddressToString
#[System.Windows.Forms.Application]::DoEvents()
start-sleep 1
}
}
}
Add-Type -AssemblyName System.Windows.Forms
$pingForm = New-Object System.Windows.Forms.Form
$label = New-Object System.Windows.Forms.Label
$button1 = New-Object System.Windows.Forms.Button
$ping = {
ContinuousPing
while($true){
Receive-Job $job
}
}
$label.Text = "Ping Status"
$button1.Location = New-Object System.Drawing.Point(100,10)
$button1.add_Click($ping)
$pingForm.Controls.Add($label)
$pingForm.Controls.Add($button1)
$pingForm.ShowDialog() | Out-Null
remove-job $job
You need to separate the GUI updating and the ping task. This is easy to in languages that are designed to have GUIs like C# winforms, If you tried it you would be surprised how much easier it is than trying to bend a GUI over PowerShell.
You can try the link Mathias posted, It will do what you require.