I have a windows form. When I click on the close (X) button of the Windows Form Control Box, I want to display a message or may be do something.
Below is the code:
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[void] [System.Windows.Forms.Application]::EnableVisualStyles()
$frmTest = New-Object System.Windows.Forms.Form
$frmTest.Size = New-Object System.Drawing.Size(640,480)
$frmTest.MaximizeBox = $False
$frmTest.ShowDialog()
When the user clicks on the Close (X) button, I want to display a message box:
$choice = [System.Windows.Forms.MessageBox]::Show('Are you you want to exit?','TEST','YesNo','Error')
switch($choice)
{
'Yes'
{
$frmTest.Close()
}
}
I found this article: Message on Form Close, but I am not sure how to use this. Please advice. Thanks
The event to capture is Closing event of the form which has an event argument which allows you to cancel the event. To learn how to use event args in PowerShell, you may want to take a look at Windows Forms Controls Events in PowerShell - Use Sender and EventArgs.
Example
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.Text ="Test"
$form.Add_Closing({param($sender,$e)
$result = [System.Windows.Forms.MessageBox]::Show(`
"Are you sure you want to exit?", `
"Close", [System.Windows.Forms.MessageBoxButtons]::YesNoCancel)
if ($result -ne [System.Windows.Forms.DialogResult]::Yes)
{
$e.Cancel= $true
}
})
$form.ShowDialog() | Out-Null
$form.Dispose()
Related
I'm facing a problem that I cannot get solved or understand with playing music from Powershell, I made a WPF GUI on top of my Powershell script.
It all works perfect except that when I press the play music button I made the music starts but after a few seconds stops.
Or when moving the mouse over the WPF GUI the music stops and I cannot get it solved. When I throw the code for playing the music in the project it works flawless, only when I assign a button to it the problems start.
So I made a stripped down version with a simple old form and a button nothing more, made an add_Click event to connect the button the code and tested again. Same problem again music stops playing either after a few seconds or when you move your mouse over the form.
Now I still had an old Windows 7 machine hanging around with Powershell V2 still on it, and guess what it worked flawlessly! Then I upgraded Powershell v2 to V5 on that machine and I had the same problem as on Win 10 (1909 with PS 5.1) laptop, so something changed with Powershell between V2 and V2 that causes this behavior, but I cannot find what.
Some examples, when I throw these lines of code in the project it works:
Add-Type -AssemblyName presentationcore
$location = (C:\users\myuserid\test.mp3)
$PlaySound = New-Object System.Windows.Media.MediaPlayer
$PlaySound.open($location)
$PlaySound.Play()
But as soon as I assign a button to it the problem as described above appears
So stripped all down to bare bones to rule out as much as I can:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName presentationcore
# Build Form
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "My Form"
$Form.Size = New-Object System.Drawing.Size(200,200)
$Form.StartPosition = "CenterScreen"
$Form.Topmost = $True
# Add Button
$Button = New-Object System.Windows.Forms.Button
$Button.Location = New-Object System.Drawing.Size(35,35)
$Button.Size = New-Object System.Drawing.Size(120,23)
$Button.Text = "Play music"
$Form.Controls.Add($Button)
#Add Button event
$Button.Add_Click({
$location = 'D:\test\test.mp3'
$PlaySound = New-Object System.Windows.Media.MediaPlayer
$PlaySound.open($location)
$PlaySound.Play()
})
#Show the Form
$form.ShowDialog()| Out-Null
So when resizing the form when the music plays will cause it to stop 95% of the time. But when I throw the code in for playing the music without the button like this it never breaks.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName presentationcore
# Build Form
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "My Form"
$Form.Size = New-Object System.Drawing.Size(200,200)
$Form.StartPosition = "CenterScreen"
$Form.Topmost = $True
# Add Button
$Button = New-Object System.Windows.Forms.Button
$Button.Location = New-Object System.Drawing.Size(35,35)
$Button.Size = New-Object System.Drawing.Size(120,23)
$Button.Text = "Play music"
$Form.Controls.Add($Button)
#Add Button event
$Button.Add_Click({
#Button now does nothing.. and music plays without breaking...ever
})
#Now it will always play to the end no matter what :-S
$location = 'D:\test\test.mp3'
$PlaySound = New-Object System.Windows.Media.MediaPlayer
$PlaySound.open($location)
$PlaySound.Play()
#Show the Form
$form.ShowDialog()| Out-Null
(Posted solution on behalf of the question author, to move it to the answer space).
I fixed the problem myself, the trick is to load the player at the beginning of your script like this:
#Clear the Console
CLS
#Determine Script location
$SCRIPT_PARENT = Split-Path -Parent $MyInvocation.MyCommand.Definition
#Add in the presentation core
Add-Type -AssemblyName presentationframework, presentationcore
#Load music player and set location here!
$location = ($SCRIPT_PARENT + "\Music.mp3")
$PlaySound = New-Object System.Windows.Media.MediaPlayer
###############################################################
# here comes a whole lot of code (XAML for WPF GUI etc etc) #
###############################################################
# Then in your event system only put:
#Play button action
$MainGUI.Playmusic.add_Click({
#Open file and play music
$PlaySound.open($location)
$PlaySound.Play()
})
This solved the playing problem 100%.
I have a button on a WinForm. After clicking on the button, a function will be called which should execute Get-ADUser cmdlet.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Data
Import-Module ActiveDirectory
$ctl_frm_aduserlist = New-Object System.Windows.Forms.Form -Property #{
Size = New-Object System.Drawing.Size(500,500)
StartPosition = "CenterScreen" }
$ctl_btn_generatepreview = New-Object System.Windows.Forms.Button -Property #{
Size = New-Object System.Drawing.Size(200,30)
Location = New-Object System.Drawing.Point(10,30)
Text = "Generate Preview" }
$ctl_frm_aduserlist.Controls.Add($ctl_btn_generatepreview)
$ctl_btn_generatepreview.Add_Click({ GeneratePreview })
function GeneratePreview(){
Write-Host "GO"
Get-ADUser -Identity "user123" -Properties Name,SamAccountName | select Name,SamAccountName
Write-Host "END" }
$ctl_frm_aduserlist.ShowDialog()
Only the two "Write-Host" cmdlets will be executed by clicking on the button.
If I only execute the single line Get-ADUser in the ISE console, it works and I get the user object.
Why does Get-ADUser not work when triggered via button?
Thanks
First off, I don't have a full solution as I'm not an expert in WinForms, to feel free to correct/complete this answer.
As far as I know, the command is actually executed. You can check it by stepping though the code or Write-Host-ing bits of the object you collected with Get-ADUser and it will display the correct info:
function GeneratePreview() {
Get-ADUser -Identity "someone" | Write-Host
}
Note that if you replace Write-Host by Write-Output it does not work anymore.
The thing is, when you make a WinForms application, the standard output isn't the console anymore. I don't know where it is redirected to, but it's not visible by default. You need to either specify that you want to see your data in the console with write-host, export it to a file (Set-Content, Export-Csv, you name it) or display it in a WinForm element:
I added a new TextBox to your form (called $TextBox1 in my example), and changed GeneratePreview like this:
function GeneratePreview() {
$user = Get-ADUser "someone"
$TextBox1.Text = "$($user.name),$($user.samaccountname)"
}
I have an interesting issue here. I'm creating a calendar picker for use when we create accounts. It works fine and is still in progress but I have noticed that when I run the script in powershell ISE, after a few minutes it locks up (I am able to edit and save the code for a few minutes prior to that). There is nothing in the event log. I get a dialog box saying that powershell is non responsive. Memory usage seems normal as well. I do not know what is happening.
This occurs no matter how I run Powershell ISE (Run as Administrator, Run as another account, and normal ISE) I am running windows 8.1.
A coworker suggested it may be the apartment model, so I've tried STA and MTA, but the problem occurs either way. It does not happen when the same code is run from the console host.
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
$objForm = New-Object Windows.Forms.Form
$objForm.Text = "Select a Date"
$objForm.Size = New-Object Drawing.Size #(490,250)
$objForm.StartPosition = "CenterScreen"
$objForm.KeyPreview = $True
$objForm.Add_KeyDown({
if ($_.KeyCode -eq "Enter")
{
$script:dtmDate=$objCalendar.SelectionStart
$objForm.Close()
}
})
$objForm.Add_KeyDown({
if ($_.KeyCode -eq "Escape")
{
$objForm.Close()
}
})
$objCalendar = New-Object System.Windows.Forms.MonthCalendar
$objCalendar.Text = "Start"
$objCalendar.ShowTodayCircle = $False
$objCalendar.MaxSelectionCount = 1
$objForm.Controls.Add($objCalendar)
$objForm.Topmost = $True
$objForm.Add_Shown({$objForm.Activate()})
[void] $objForm.ShowDialog()
if ($dtmDate)
{
Write-Host "Date selected: $dtmDate"
}
$objForm.Dispose()
In Response to #The Unique Paul Smith
function Find-CalenderDateTest {
[CmdletBinding()]
param(
[Parameter(
Mandatory=$false
)]
[ValidateSet('long','short','powerpoint')]
[ValidateNotNullOrEmpty()]
[string]
$DateFormat
)
Begin{
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
$objForm = New-Object Windows.Forms.Form
$objForm.Text = "Select a Date"
$objForm.Size = New-Object Drawing.Size #(243,250)
$objForm.StartPosition = "CenterScreen"
$objForm.KeyPreview = $True
$dtmDate = $null
$objForm.Add_KeyDown( {
if ($_.KeyCode -eq "Enter")
{
$dtmDate=$objCalendar.SelectionStart
$objForm.Close()
}
})
$objForm.Add_KeyDown({
if ($_.KeyCode -eq "Escape")
{
$objForm.Close()
}
})
#region OK Button
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Size(20,175)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"
# Got rid of the Click event for OK Button, and instead just assigned its DialogResult property to OK.
$OKButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$objForm.Controls.Add($OKButton)
# Setting the form's AcceptButton property causes it to automatically intercept the Enter keystroke and
# treat it as clicking the OK button (without having to write your own KeyDown events).
$objForm.AcceptButton = $OKButton
#endregion
#region Cancel Button
$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Size(80,175)
$CancelButton.Size = New-Object System.Drawing.Size(75,23)
$CancelButton.Text = "Cancel"
# Got rid of the Click event for Cancel Button, and instead just assigned its DialogResult property to Cancel.
$CancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$objForm.Controls.Add($CancelButton)
# Setting the form's CancelButton property causes it to automatically intercept the Escape keystroke and
# treat it as clicking the OK button (without having to write your own KeyDown events).
$objForm.CancelButton = $CancelButton
#endregion
$objCalendar = New-Object System.Windows.Forms.MonthCalendar
$objCalendar.ShowTodayCircle = $False
$objCalendar.MaxSelectionCount = 1
$objForm.Controls.Add($objCalendar)
$objForm.Topmost = $True
$objForm.Add_Shown({$objForm.Activate()})
$Results = $objForm.ShowDialog()
}
Process{}
End{
if ($Results -eq "OK")
{
$objCalendar.SelectionStart
}
$objForm.Dispose()
}
}
The error is MTA/STA
Don't use
$form.showDialog()
Use
[system.windows.forms.application]::run($form)
instead
and it works fine every time
Another way is to put it in another thread:
$code
{
//form code here
$form.showDialog()
}
$newThread = [Powershell]::Create()
$newThread.AddScript($code)
$handle = $newThread.BeginInvoke()
Provide variables from the calling script:
$newThread.Runspace.SessionStateProxy.SetVariable("variablenname",value)
before the BeginInvoke use variablenname without $...
It's a long shot but the problem might be that powershell is not closing the $objForm object correctly, leaving it running in memory while the ISE waits for input after the script has terminated. If you check your taskmanager, is the form still running in the background? You could also try adding 'Remove-Variable objForm' (no $) after the dispose() and see if that helps.
More information: https://technet.microsoft.com/en-us/library/ff730962.aspx
As I say, it's a long shot.
I was using combobox.items.add:
$configCombo.Items.Add($wks)
and I looked up how to keep the keys from printing to the console - and changed the add to:
[void]$configCombo.Items.Add($wks)
Since then I have added the void - I have been running it in ISE and it has not hung since.
Ran into this issue too. Generally occurs when I lock my workstation and return. After a bit of poking about and googleing, I found this
https://support.microsoft.com/en-us/help/943139/windows-forms-application-freezes-when-system-settings-are-changed-or, which seems like the issue at hand.
Issue
The application will not respond and the UI thread will hang in an
Invoke call while handling the OnUserPreferenceChanged notification
Cause
This occurs if a control is created on a thread which doesn't pump
messages and the UI thread receives a WM_SETTINGCHANGE message.
Resolution
Applications should never leave Control objects on threads without an
active message pump. If Controls cannot be created on the main UI
thread, they should be created on a dedicated secondary UI thread and
Disposed as soon as they are no longer needed.
I had the same issue, but the solution is: Always clean up right after the form is done.
In this case:
$objForm.Dispose()
Up to now (a few hours) I didn't have that issue again. Previously it locked up after > 10 Minutes.
I'm using WinForms with PowerShell. In my tool, I'd like a checkbox that when checked, will display a message next to it, and when unchecked, it will remove the message.
I've gotten this far (I'm sure there's a much better way to do it). This makes the message pop-up, but it doesn't go away when you uncheck the box. Any help would be appreciated. Thanks!
$Checkbox_Errors.Add_CheckStateChanged({ ### Checkbox_Errors is the name of the checkbox
if ($Checkbox_Errors.Checked -eq $true)
{
$ErrorWarning1 = New-Object System.Windows.Forms.Label
$ErrorWarning1.Text = "WARNING: May take 3-5 Minutes" ### When checked, this is what it should display
$ErrorWarning1.ForeColor = "Red"
$ErrorWarning1.AutoSize = $True
$ErrorWarning1.Location = new-object System.Drawing.Point(170,13)
$groupbox.Controls.Add($ErrorWarning1)
}
})
$Checkbox_Errors.Add_CheckStateChanged({
if ($Checkbox_Errors.Unchecked -eq $true)
{
$ErrorWarning1 = New-Object System.Windows.Forms.Label
$ErrorWarning1.Text = "" ### I attempted this, where it would re-write
$ErrorWarning1.ForeColor = "Red"
$ErrorWarning1.AutoSize = $True
$ErrorWarning1.Location = new-object System.Drawing.Point(170,13)
$groupbox.Controls.Add($ErrorWarning1)
}
})
It would be simpler to create the label with the rest of your form elements. If you're using a designer, you can just drag it on with the rest of the controls. Then set the label's Visible property to $False initially to hide it.
$ErrorWarning1 = New-Object System.Windows.Forms.Label
$ErrorWarning1.Text = "WARNING: May take 3-5 Minutes"
$ErrorWarning1.ForeColor = "Red"
$ErrorWarning1.AutoSize = $True
$ErrorWarning1.Location = new-object System.Drawing.Point(170,13)
$ErrorWarning1.Visible = $False # This line hides the label initially
$groupbox.Controls.Add($ErrorWarning1)
Now in your event handler, instead of generating the label, just show or hide it based on the state of the checkbox:
$ErrorWarning1.Visible = $Checkbox_Errors.Checked
The label will always exist, but it will only be visible when the checkbox is checked.
If I run the following code, the Event Action is executed:
$Job = Start-Job {'abc'}
Register-ObjectEvent -InputObject $Job -EventName StateChanged `
-Action {
Start-Sleep -Seconds 1
Write-Host '*Event-Action*'
}
The string 'Event-Action' is displayed.
If I use a Form and start the above code by clicking a button,
the Event Action is not executed:
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form1 = New-Object Windows.Forms.Form
$Form1.Add_Shown({
$Form1.Activate()
})
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Text = 'Test'
$Form1.Controls.Add($Button1)
$Button1.Add_Click({
Write-Host 'Test-Button was clicked'
$Job = Start-Job {'abc'}
Register-ObjectEvent -InputObject $Job -EventName StateChanged `
-Action {
Start-Sleep -Seconds 1
Write-Host '*Event-Action*'
}
})
$Form1.ShowDialog()
Only when I click the button again, the first Event Action is executed.
With the third click the second Event Action is executed and so on.
If I do multiple clicks in rapid succession, the result is unpredictable.
Furthermore when I close the form with the button in the upper right corner,
the last "open" Event Action is executed.
Note: For testing PowerShell ISE is to be preferred, because PS Console displays
the string only under certain circumstances.
Can someone please give me a clue what's going on here?
Thanks in advance!
nimizen.
Thanks for your explanation, but I don't really understand, why the StateChanged event is not fired or visible to the main script until there is some action with the Form. I'd appreciate another attempt to explain it to me.
What I want to accomplish is a kind of multithreading with PowerShell and Forms.
My plan is the following:
'
The script shows a Form to the user.
The user does some input and clicks a button.
Based on the user's input a set of Jobs are started with Start-Job and a StateChanged event is registered for each job.
While the Jobs are running, the user can perform any action on the Form (including stop the Jobs via a button) and the Form is repainted when necessary.
The script reacts to any events which are fired by the Form or its child controls.
Also the script reacts to each job's StateChanged event.
When a StateChanged event occurs, the state of each job is inspected, and if all jobs have the state 'Completed', the jobs' results are fetched with Receive-Job and displayed to the user.
'
All this works fine except that the StateChanged event is not visible to the main script.
The above is still my favorite solution and if you have any idea how to implement this, please let me know.
Otherwise I'll most likely resort to a workaround, which at least gives the user a multithreading feeling. It is illustrated in the following example:
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form1 = New-Object Windows.Forms.Form
$Form1.Add_Shown({
$Form1.Activate()
})
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Text = 'Test'
$Form1.Controls.Add($Button1)
$Button1.Add_Click({
$Form1.Focus()
Write-Host 'Test-Button was clicked'
$Job = Start-Job {Start-Sleep -Seconds 1; 'abc'}
Do {
Start-Sleep -Milliseconds 100
Write-Host 'JobState: ' $Job.State
[System.Windows.Forms.Application]::DoEvents()
}
Until ($Job.State -eq 'Completed')
Write-Host '*Action*'
})
$Form1.ShowDialog()
There are a lot of (StackOverflow) questions and answers about this ‘enduring mystique’ of combining form (or WPF) events with .NET events (like EngineEvents, ObjectEvents and WmiEvents) in PowerShell:
Do Jobs really work in background in powershell?
WPF events not working in Powershell - Carousel like feature in multi-threaded script
is it possible to control WMI events though runspace and the main form?
Is there a way to send events to the parent job when using Start-WPFJob?
Update WPF DataGrid ItemSource from Another Thread in PowerShell
They are all come down two one point: even there are multiple threads setup, there are two different 'listeners' in one thread. When your script is ready to receive form events (using ShowDialog or DoEvents) it can’t listen to .NET events at the same time. And visa versa: if script is open for .NET events while processing commands (like Start-Sleep or specifically listen for .NET events using commands like Wait-Event or Wait-Job), your form will not be able to listen to form events. Meaning that either the .NET events or the form events are being queued simply because your form is in the same thread as the .NET listener(s) your trying to create.
As with the nimizen example, with looks to be correct at the first glans, your form will be irresponsive to all other form events (button clicks) at the moment you’re checking the backgroundworker’s state and you have to click the button over and over again to find out whether it is still ‘*Doing Stuff’. To work around this, you might consider to combine the DoEvents method in a loop while you continuously checking the backgroundworker’s state but that doesn’t look to be a good way either, see: Use of Application.DoEvents()
So the only way out (I see) is to have one thread to trigger the form in the other thread which I think can only be done with using [runspacefactory]::CreateRunspace() as it is able to synchronize a form control between the treats and with that directly trigger a form event (as e.g. TextChanged).
(if there in another way, I eager to learn how and see a working example.)
Form example:
Function Start-Worker {
$SyncHash = [hashtable]::Synchronized(#{TextBox = $TextBox})
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ThreadOptions = "UseNewThread" # Also Consider: ReuseThread
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash)
$Worker = [PowerShell]::Create().AddScript({
$ThreadID = [appdomain]::GetCurrentThreadId()
$SyncHash.TextBox.Text = "Thread $ThreadID has started"
for($Progress = 0; $Progress -le 100; $Progress += 10) {
$SyncHash.TextBox.Text = "Thread $ThreadID at $Progress%"
Start-Sleep 1 # Some background work
}
$SyncHash.TextBox.Text = "Thread $ThreadID has finnished"
})
$Worker.Runspace = $Runspace
$Worker.BeginInvoke()
}
[Void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form = New-Object Windows.Forms.Form
$TextBox = New-Object Windows.Forms.TextBox
$TextBox.Visible = $False
$TextBox.Add_TextChanged({Write-Host $TextBox.Text})
$Form.Controls.Add($TextBox)
$Button = New-Object System.Windows.Forms.Button
$Button.Text = "Start worker"
$Button.Add_Click({Start-Worker})
$Form.Controls.Add($Button)
$Form.ShowDialog()
For a WPF example, see: Write PowerShell Output (as it happens) to WPF UI Control
The state property of Powershell jobs is read-only; this means that you can't configure the job state to be anything before you actually start the job. When you're monitoring for the statechanged event, it doesn't fire until the click event comes around again and the state is 'seen' to change from 'running' to 'completed' at which point your script block executes. This is also the reason why the scriptblock executes when closing the form.
The following script removes the need to monitor the event and instead monitors the state. I assume you want to fire the on 'statechanged' code when the state is 'running'.
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form1 = New-Object Windows.Forms.Form
$Form1.Add_Shown({
$Form1.Activate()
})
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Text = 'Test'
$Form1.Controls.Add($Button1)
$Button1.Add_Click({
$this.Enabled = $false
Write-Host $Job.State " - (Before job started)"
$Job = Start-Job {'abc'}
Write-Host $Job.State " - (After job started)"
If ($Job.State -eq 'Running') {
Start-Sleep -Seconds 1
Write-Host '*Doing Stuff*'
}
Write-Host $Job.State " - (After IF scriptblock finished)"
[System.Windows.Forms.Application]::DoEvents()
$this.Enabled = $true
})
$Form1.ShowDialog()
In addition, note the lines:
$this.Enabled = $false
[System.Windows.Forms.Application]::DoEvents()
$this.Enabled = $true
These lines ensure the button doesn't queue click events. You can obviously remove the 'write-host' lines, I've left those in so you can see how the state changes as the script executes.
Hope this helps.