Multi Monitor screens

Hi,

I am writing a script that will be run from an SCCM task sequence. Basically what happens is the task sequence will set up a scheduled task to run at logon, this in turn will run my script. The script is very basic in that it just throws up an xaml form with an install button and a progress bar. I have taken out the below in the code in the xaml, so it is easier to test.

WindowState=“Maximized”
WindowStyle=“None”

My issue is not everyone will have a single monitor and I need to blank them out as well maybe up to 4 monitors, my first thought is a second form either another xaml form or use:-

[void] [System.Reflection.Assembly]::LoadWithPartialName(“System.Drawing”)
[void] [System.Reflection.Assembly]::LoadWithPartialName(“System.Windows.Forms”)

To generate the second form and set its size as a mile high by a mile wide.
The issue is if the user has not set the topmost left or their left monitor as the main screen the second form will not blank out this monitor.

So the question is how to display the second form that covers all of the monitors with a blank form irrispective of how the users have their monitor configured then display my current form on their main monitor ?

Sorry for the amount of code

$Global:syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = “STA”
$newRunspace.ThreadOptions = “ReuseThread”
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable(“syncHash”,$syncHash)

Load WPF assembly if necessary

[void][System.Reflection.Assembly]::LoadWithPartialName(‘presentationframework’)

$psCmd = [PowerShell]::Create().AddScript({
[xml]$xaml = @"

"@

$reader=(New-Object System.Xml.XmlNodeReader $xaml)

$syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

[xml]$XAML = $xaml
    $xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | %{
    #Find all of the form types and add them as members to the synchash
    $syncHash.Add($_.Name,$syncHash.Window.FindName($_.Name) )

}

$Script:JobCleanup = [hashtable]::Synchronized(@{})
$Script:Jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList))

#region Background runspace to clean up jobs
$jobCleanup.Flag = $True
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"          
$newRunspace.Open()        
$newRunspace.SessionStateProxy.SetVariable("jobCleanup",$jobCleanup)     
$newRunspace.SessionStateProxy.SetVariable("jobs",$jobs) 
$jobCleanup.PowerShell = [PowerShell]::Create().AddScript({
    #Routine to handle completed runspaces
    Do {    
        Foreach($runspace in $jobs) {            
            If ($runspace.Runspace.isCompleted) {
                [void]$runspace.powershell.EndInvoke($runspace.Runspace)
                $runspace.powershell.dispose()
                $runspace.Runspace = $null
                $runspace.powershell = $null               
            } 
        }
        #Clean out unused runspace jobs
        $temphash = $jobs.clone()
        $temphash | Where {
            $_.runspace -eq $Null
        } | ForEach {
            $jobs.remove($_)
        }        
        Start-Sleep -Seconds 1     
    } while ($jobCleanup.Flag)
})
$jobCleanup.PowerShell.Runspace = $newRunspace
$jobCleanup.Thread = $jobCleanup.PowerShell.BeginInvoke()  
#endregion Background runspace to clean up jobs

Function Update-Display {
Param (
$Control,
$Property,
$Value,
[switch]$AppendContent
)

    # This is kind of a hack, there may be a better way to do this
    If ($Property -eq "Close") {
        $syncHash.Window.Dispatcher.invoke([action]{$syncHash.Window.Close()},"Normal")
        Return
    }

    # This updates the control based on the parameters passed to the function
    $syncHash.$Control.Dispatcher.Invoke([action]{
        # This bit is only really meaningful for the TextBox control, which might be useful for logging progress steps
        If ($PSBoundParameters['AppendContent']) {
            $syncHash.$Control.AppendText($Value)
        } Else {
            $syncHash.$Control.$Property = $Value
        }
    }, "Normal")
} 

$DisplayText = "

This is a TextBlock control
with multiple lines of text.
3rd Line
4th Line"

Update-Display -control TextDisplay -Property Text -Value $DisplayText

$syncHash.Install.Add_Click({
  
    $newRunspace =[runspacefactory]::CreateRunspace()
    $newRunspace.ApartmentState = "STA"
    $newRunspace.ThreadOptions = "ReuseThread"          
    $newRunspace.Open()
    $newRunspace.SessionStateProxy.SetVariable("SyncHash",$SyncHash) 
    $PowerShell = [PowerShell]::Create().AddScript({

Function Update-Window {
Param (
$Control,
$Property,
$Value,
[switch]$AppendContent
)

    # This is kind of a hack, there may be a better way to do this
    If ($Property -eq "Close") {
        $syncHash.Window.Dispatcher.invoke([action]{$syncHash.Window.Close()},"Normal")
        Return
    }

    # This updates the control based on the parameters passed to the function
    $syncHash.$Control.Dispatcher.Invoke([action]{
        # This bit is only really meaningful for the TextBox control, which might be useful for logging progress steps
        If ($PSBoundParameters['AppendContent']) {
            $syncHash.$Control.AppendText($Value)
        } Else {
            $syncHash.$Control.$Property = $Value
        }
    }, "Normal")
}                        

start-sleep -Milliseconds 850

$scripttorun = “some powershell script.ps1”

update-window -Control Progress -Property Value -Value 25

start-sleep -Milliseconds 850
update-window -Control Progress -Property Value -Value 50
Invoke-Expression $scripttorun

start-sleep -Milliseconds 500
update-window -Control Progress -Property Value -Value 75

start-sleep -Milliseconds 200
update-window -Control Progress -Property Value -Value 100
})
$PowerShell.Runspace = $newRunspace
[void]$Jobs.Add((
[pscustomobject]@{
PowerShell = $PowerShell
Runspace = $PowerShell.BeginInvoke()
}
))
})

#region Window Close 
$syncHash.Window.Add_Closed({
    Write-Verbose 'Halt runspace cleanup job processing'
    $jobCleanup.Flag = $False

    #Stop all runspaces
    $jobCleanup.PowerShell.Dispose()      
})
#endregion Window Close 

$syncHash.Window.ShowDialog() | Out-Null
$syncHash.Error = $Error

})

$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

I just finished coding this for some scripts we are running. I have my code just before I create my event actions, $synchash.install.Add_Click. Rather than using a here-string for the xaml, I’ve separated that out into a separate file, but it shouldn’t be too hard to proceed either way you go. I’ve added the xaml code a the bottom.

Essentially I create a new window, in a separate runspace, for each monitor that is not “primary”. I then set the window bounds to be whatever the screen bounds are.

Let me know if you have any questions.

#region screenblocker
    Add-Type -AssemblyName System.Windows.Forms
    $screens = [System.Windows.Forms.Screen]::Allscreens | Where-Object Primary -eq $false
    $synchash.screens = New-Object System.Collections.Generic.List[System.Object]
    foreach($screen in $screens){ 
        $synchash.screens.Add($screen.devicename.replace('\','').replace('.','')[-1])
    }
    foreach ($screen in $screens){
        $synchash."$($screen.devicename.replace('\','').replace('.',''))" = $screen
        $newRunspace =[runspacefactory]::CreateRunspace()
        $newRunspace.ApartmentState = "STA"
        $newRunspace.ThreadOptions = "ReuseThread"
        $newRunspace.Open()
        $newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
        $PowerShell = [PowerShell]::Create().AddScript({
            function LoadXaml ($filename){
                $XamlLoader=(New-Object System.Xml.XmlDocument)
                $XamlLoader.Load($filename)
                return $XamlLoader
            }
            $screenNum = $synchash.screens[0]
            $display = "Display" + $screenNum
            $synchash.screens.RemoveAt(0)

            
            $XamlMainWindow = LoadXaml("\blankScreen.xaml")
            $reader = (New-Object System.Xml.XmlNodeReader $XamlMainWindow)
            $syncHash."Window$screenNum" = [Windows.Markup.XamlReader]::Load($reader)
        
        
        
            [xml]$XAML = $XamlMainWindow
            $XamlMainWindow.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object{
            #Find all of the form types and add them as members to the synchash
            $syncHash.Add($("" + $_.Name + $screenNum),$syncHash."Window$screenNum".FindName($_.Name) )
            }

            $syncHash."Window$screenNum".Top = $synchash.$display.bounds.Top;
            $syncHash."Window$screenNum".Left = $synchash.$display.bounds.Left;
            $syncHash."Window$screenNum".Width = $synchash.$display.bounds.Width;
            $syncHash."Window$screenNum".Height = $synchash.$display.bounds.Height;
            $synchash."Window$screenNum".Show()

            $synchash."error$screenNum" = $error
        })
        $PowerShell.Runspace = $newRunspace
        [void]$Jobs.Add((
            [pscustomobject]@{
                PowerShell = $PowerShell
                Runspace = $PowerShell.BeginInvoke()
            }
        ))
    }
    #endregion screenblocker

It didn’t like the xml, so I just removed the opening brackets. Be sure to add them back.

Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    ResizeMode="NoResize" WindowStartupLocation="Manual" Topmost="True" ShowInTaskbar="False" WindowStyle='None'>
    
    
/Window>
    
    

thanks for your script. I will give it a go.

Thanks that worked a treat. Only issue now is that some users on Windows 10 have pinned their task bar to always show so I need to find a way of hiding it so the main screen is the only one they see