Global (system-wide) hotkey with Powershell - winforms

i have a winform waitng in the back and would like to make it appear with a global (system wide) hotkey. (see bogus "new-hotkey"-cmdlet below)
I searched the web intensively but only found C# solutions, which i could not translate into powershell (v3).
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$form1 = New-Object 'System.Windows.Forms.Form'
$form1.ClientSize = '300, 250'
$form1.windowstate = "minimized"
# -- bogus code --
$hk = new-hotkey -scope Global -keys "Ctrl,Shift,Z" -action ({$form1.windowstate = "normal" })
# ----------------
$form1.ShowDialog()
thanks, Rob

Related

Playing music in Powershell stops when assigned to button

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%.

Adding elements to a form from another runspace

I have a form in which as soon as ready several elements will be added (for example, a list). It may take some time to add them (from fractions of a second to several minutes). Therefore, I want to add processing to a separate thread (child). The number of elements is not known in advance (for example, how many files are in the folder), so they are created in the child stream. When the processing in the child stream ends, I want to display these elements on the main form (before that the form did not have these elements and performed other tasks).
However, I am faced with the fact that I cannot add these elements to the main form from the child stream. I will give a simple example as an example. It certainly works:
$Main = New-Object System.Windows.Forms.Form
$Run = {
# The form is busy while adding elements (buttons here)
$Top = 0
1..5 | % {
$Button = New-Object System.Windows.Forms.Button
$Button.Top = $Top
$Main.Controls.Add($Button)
$Top += 30
Sleep 1
}
}
$Main.Add_Shown($Run)
# Adding and performing other tasks on the form here
[void]$Main.ShowDialog()
But, adding the same thing to the child stream I did not get the button to display on the main form. I do not understand why.
$Main = New-Object System.Windows.Forms.Form
$Run = {
$RS = [Runspacefactory]::CreateRunspace()
$RS.Open()
$RS.SessionStateProxy.SetVariable('Main', $Main)
$PS = [PowerShell]::Create().AddScript({
# Many items will be added here. Their number and processing time are unknown in advance
# Now an example with the addition of five buttons.
$Top = 0
1..5 | % {
$Button = New-Object System.Windows.Forms.Button
$Button.Top = $Top
$Main.Controls.Add($Button)
$Top += 30
Sleep 1
}
})
$PS.Runspace = $RS; $Null = $PS.BeginInvoke()
}
$Main.Add_Shown($Run)
[void]$Main.ShowDialog()
How can I add elements to the main form that are created in the child stream? thanks
While you can create controls on thread B, you cannot add them to a control that was created in thread A from thread B.
If you attempt that, you'll get the following exception:
Controls created on one thread cannot be parented to a control on a different thread.
Parenting to means calling the .Add() or .AddRange() method on a control (form) to add other controls as child controls.
In other words: In order to add controls to your $Main form, which is created and later displayed in the original thread (PowerShell runspace), the $Main.Controls.Add() call must occur in that same thread.
Similarly, you should always attach event delegates (event-handler script blocks) in that same thread too.
While your own answer attempts to ensure adding the buttons to the form in the original runspace, it doesn't work as written - see the bottom section.
I suggest a simpler approach:
Use a thread job to create the controls in the background, via Start-ThreadJob.
Start-ThreadJob is part of the the ThreadJob module that offers a lightweight, thread-based alternative to the child-process-based regular background jobs and is also a more convenient alternative to creating runspaces via the PowerShell SDK.
It comes with PowerShell [Core] v6+ and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser.
In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.
Show your form non-modally (.Show() rather than .ShowDialog()) and process GUI events in a [System.Windows.Forms.Application]::DoEvents() loop.
Note: [System.Windows.Forms.Application]::DoEvents() can be problematic in general (it is essentially what the blocking .ShowDialog() call does behind the scenes), but in this constrained scenario (assuming only one form is to be shown) it should be fine. See this answer for background information.
In the loop, check for newly created buttons as output by the thread job, attach an event handler, and add them to your form.
Here is a working example that adds 3 buttons to the form after making it visible, one after the other while sleeping in between:
Add-Type -ea Stop -Assembly System.Windows.Forms
$Main = New-Object System.Windows.Forms.Form
# Start a thread job that will create the buttons.
$job = Start-ThreadJob {
$top = 0
1..3 | % {
# Create and output a button object.
($btn = [System.Windows.Forms.Button] #{
Name = "Button$_"
Text = "Button$_"
Top = $top
})
Start-Sleep 1
$top += $btn.Height
}
}
# Show the form asynchronously
$Main.Show()
# Process GUI events in a loop, and add
# buttons to the form as they're being created
# by the thread job.
while ($Main.Visible) {
[System.Windows.Forms.Application]::DoEvents()
if ($button = Receive-Job -Job $job) {
# Add an event handler...
$button.add_Click({ Write-Host "Button clicked: $($this.Name)" })
# .. and it to the form.
$Main.Controls.AddRange($button)
}
}
# Clean up.
$Main.Dispose()
Remove-Job -Job $job -Force
'Done'
As of this writing, your own answer tries to achieve adding the controls to the form in the original runspace by using Register-ObjectEvent to subscribe to the other thread's (runspace's) events, given that the -Action script block used for event handling runs (in a dynamic module inside) the original thread (runspace), but there are two problems with that:
Unlike your answer suggests, the -Action script block neither directly sees the $Main variable from the original runspace, nor the other runspace's variables - these problems can be overcome, however, by passing $Main to Register-ObjectEvent via -MessageData and accessing it via $Event.MessageData in the script block, and by accessing the other runspace's variables via $Sender.Runspace.SessionStateProxy.GetVariable() calls.
More importantly, however, the .ShowDialog() call will block further processing; that is, your events won't fire and therefore your -Action script block won't be invoked until after the form closes.
Update: You mention a workaround in order to get PowerShell's events to fire while the form is being displayed:
Subscribe to the MouseMove event with a dummy event handler whose invocation gives PowerShell a chance to fire its own events while the form is being displayed modally; e.g.: $Main.Add_MouseMove({ Out-Host }); note that this workaround is only effective if the script block
calls a command, such as Out-Host in this example (which is effectively a no-op); a mere expression or .NET method call is not enough.
However, this workaround is suboptimal in that it relies on the user (continually) mousing over the form for the PowerShell events to fire; also, it is somewhat obscure and inefficient.
I think you can't create form and controls on different threads. But you can access control properties though. So you can create form with control placeholders in a runspace, then change them on the main thread once your calculations are complete. Example:
$form = New-Object System.Windows.Forms.Form
$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$rs.ApartmentState = [System.Threading.ApartmentState]::MTA
$ps = [powershell]::create()
$ps.Runspace = $rs
$rs.Open()
$out = $ps.AddScript({param($form)
$button1 = New-Object System.Windows.Forms.Button
$button1.Name = "button1"
$form.Controls.Add($button1)
$form.ShowDialog()
}).AddArgument($form).BeginInvoke()
#-----------------------------
sleep 1;
$form.Controls["button1"].Text = "some button"
This is the way I'm using now. Thanks to #mklement0 for the talk about the Register-ObjectEvent method (here). I applied it here. The essence of the method is that the elements are created in the child stream (in this case, the Button), and when the child space has finished work, Register-ObjectEvent is processed. Register-ObjectEvent is located in the main space and therefore allows you to add an element (Button here) to the form.
$Main = New-Object System.Windows.Forms.Form
$Run = {
$RS = [Runspacefactory]::CreateRunspace()
$RS.Open()
$RS.SessionStateProxy.SetVariable('Main', $Main)
$PS = [PowerShell]::Create().AddScript({
$Button = New-Object System.Windows.Forms.Button
}
})
$PS.Runspace = $RS
$Null = Register-ObjectEvent -InputObject $PS -EventName InvocationStateChanged -Action {
if ($EventArgs.InvocationStateInfo.State -in 'Completed', 'Failed') {
$Main.Controls.Add($Button)
}
}
$Null = $PS.BeginInvoke()
}
$Main.Add_Shown($Run)
[void]$Main.ShowDialog()
This is my a workaround. However, I still do not know if it is possible in principle to add elements of a child space to a form from a child space. This, of course, is about adding, not managing, because managing from the child space is successful.
Putting this here, since it is too long for the regular comment section.
Now, I don't spend much time using runspaces in production, as I've no had a real need (at least to date) for them, in class, sure, but I digress.
However, from all my previous readings and notes I've kept, this sounds like a use case for RunSpace Pools. Here are three of my saved resources. Working under the assumption that you may have not seen all of them of course. Now, I would post their code as well, but all are very long, so, there's that. Based on your use case, it could be seen as a duplicate to the last link resource.
PowerShell and WPF: Writing Data to a UI From a Different
Runspace
PowerShell Tip: Utilizing Runspaces for Responsive WPF GUI
Applications
Sharing Variables and Live Objects Between PowerShell Runspaces
How to access a different powershell runspace without WPF-object

Get Attributes listed in the "Details" Tab with Powershell

I'm trying to retrieve some extended file attributes (mp3 files to be precise) listed in the "Details" tab, section "Media" with PowerShell.
There are additional attributes like bitrate, genre, album, etc.
Sadly Get-ItemProperty -Path $pathtofile | Format-List does not return those.
Get-ItemProperty -Path $pathtofile | Get-Member does not list them either.
Strange enough, Get-ItemProperty returns the same output as Get-ChildItem.
I've tried different files (even non mp3s) and PowerShell does not list the "detail" attributes anywhere.
Where does windows store these? Also how can one list them?
Update 3;
I found a better script that should do exactly what you want provided by the incredible "Hey! Scripting Guy" blog. They already built a function that lets you view all of the details of music/mp3 files.
Blog post
https://blogs.technet.microsoft.com/heyscriptingguy/2014/02/05/list-music-file-metadata-in-a-csv-and-open-in-excel-with-powershell/
Function
https://gallery.technet.microsoft.com/scriptcenter/get-file-meta-data-function-f9e8d804
I had a look at Ed Wilson's post. I can see he wrote it back in the day on Windows Vista. Powershell has changed a bit since then. I've tweaked the code he had written to work on Win 10. Hope this helps!
# Converted from Ed Wilson: https://devblogs.microsoft.com/scripting/hey-scripting-guy-how-can-i-find-files-metadata/
param($folder = "C:\Test") #end param
function funLinestrIN($strIN)
{
$strLine = "=" * $strIn.length
Write-Host "`n$strIN" -ForegroundColor Yellow
Write-Host $strLine -ForegroundColor Cyan
} #end funline
function funMetaData
{
foreach($sFolder in $folder)
{
$a = 0
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.namespace($sFolder)
foreach ($strFileName in $objFolder.items())
{
funLinestrIN( "$($strFileName.name)")
for ($a ; $a -le 266; $a++)
{
if($objFolder.getDetailsOf($strFileName, $a))
{
$hash += #{ `
$($objFolder.getDetailsOf($objFolder.items, $a)) = $($objFolder.getDetailsOf($strFileName, $a))
} #end hash
$hash
$hash.clear()
} #end if
} #end for
$a=0
} #end foreach
} #end foreach
} #end funMetadata
funMetaData # run function
This answer is just to fix the link in Nick W's answer. Microsoft killed TechNet but the script is on PowerShell Gallery.
I googled the following to find it:
PowerShell "Get-FileMetaData"
The location is:
https://www.powershellgallery.com/packages/FC_SysAdmin/5.0.0/Content/public%5CGet-FileMetaData.ps1
Unfortunately, I can't get it to work, lol.

Power shell custom function in script

I had prepared a script to pull some report w.r.t SQL server and out put will be pushed to different CSV sheets. After output is generated, all the CSV's are merged to single Excel file with the help of custom created function and that excel will be sent to my email address.
While running htrough powershell_ise.exe, it is running fine and I am receiving the email successfully. When I scheduled the same script, I am receiving the email but with out excel attachments. I am suspecting that custom created function is not used, because I dont see any converted excel files in the desired location.
I tried all possible ways, like dot sourching, pasting the function in the script itself but no luck.
I am a beginner in powershell, can some one please help me if i am missing some thing.
Thanks,
Anil
Function Merge-CSVFiles
{
Param(
$CSVPath = "D:\Anil\Missing_Indexes", ## Soruce CSV Folder
$XLOutput="D:\Anil\Missing_Indexes.xls" ## Output file name
)
$csvFiles = Get-ChildItem ("$CSVPath\*") -Include *.csv
$Excel = New-Object -ComObject excel.application
$Excel.visible = $false
$Excel.sheetsInNewWorkbook = $csvFiles.Count
$workbooks = $excel.Workbooks.Add()
$CSVSheet = 1
Foreach ($CSV in $Csvfiles)
{
$worksheets = $workbooks.worksheets
$CSVFullPath = $CSV.FullName
$SheetName = ($CSV.name -split "\.")[0]
$worksheet = $worksheets.Item($CSVSheet)
$worksheet.Name = $SheetName
$TxtConnector = ("TEXT;" + $CSVFullPath)
$CellRef = $worksheet.Range("A1")
$Connector = $worksheet.QueryTables.add($TxtConnector,$CellRef)
$worksheet.QueryTables.item($Connector.name).TextFileCommaDelimiter = $True
$worksheet.QueryTables.item($Connector.name).TextFileParseType = 1
$worksheet.QueryTables.item($Connector.name).Refresh()
$worksheet.QueryTables.item($Connector.name).delete()
$worksheet.UsedRange.EntireColumn.AutoFit()
$CSVSheet++
}
$workbooks.SaveAs($XLOutput,51)
$workbooks.Saved = $true
$workbooks.Close()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbooks) | Out-Null
$excel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
While running through powershell_ise.exe, it is running fine and I am receiving the email successfully
This is because the ISE on your box is able to load your custom function during runtime and generate the report.
But when this is scheduled through a job, the script will run on a different server than your box, hence the script will not be able to find the function and you don't get the report.
I have faced this kind of issue before while using custom functions.
The work around that i can suggest is wrapping for custom functions in a separate module and importing the module in your main script. ( preferably save the module in the same location as your script for easy troubleshooting).
Example:
Save your function in a .psm1 module file
Function ScriptExample {
Param ( [string] $script,
[string] $jobname,
[string] $jobcategory,
[hashtable] $config,
[string] $deletelogfilepath,
[string] $servername)
#your-function-here#
{
}
Return $script;
}
Now call this module in your main script as follows,
$importmoduleroot = "C:\temp\SO_Example.psm1"
###### LOAD MODULES ######
# Import all related modules written to support this process
$modules = get-childitem -path $importmoduleroot -include SO*.psm1 -recurse;
You can then call your function and pass in the parameters within the main script,
ScriptExample -script $script `
-jobname $jobname `
-jobcategory $jobcategory `
-config $config `
-servername $ServerName `
-deletelogfilepath $deletelogfilepath

Powershell pack uri object

I'm trying to create a pack ui referencing a xaml resource inside of an assembly file in powershell. After reading this post I tried to do this:
$resource = new-object system.uri("pack://application:,,,/WPFResource;component/test.xaml")
The I get an error noting that it is expecting a port since there are two colons.
Can anyone please advice?
You can go about this one of two ways. One is to load up and init the WPF infrastructure:
Add-Type -AssemblyName PresentationFramework,PresentationCore
[windows.application]::current > $null # Inits the pack protocol
new-object system.uri("pack://application:,,,/WPFResource;component/test.xaml")
The other way is to manually register the pack protocol:
$opt = [GenericUriParserOptions]::GenericAuthority
$parser = new-object system.GenericUriParser $opt
if (![UriParser]::IsKnownScheme("pack")) {
[UriParser]::Register($parser,"pack",-1)
}
new-object system.uri("pack://application:,,,/WPFResource;component/test.xaml")

Resources