Multi Monitor screens

This topic contains 3 replies, has 2 voices, and was last updated by  Simon B 2 months, 1 week ago.

  • Author
    Posts
  • #95916

    Simon B
    Participant

    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()

  • #95928

    Will Prather
    Participant

    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>
        
        
    
    
  • #96008

    Simon B
    Participant

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

  • #96012

    Simon B
    Participant

    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

You must be logged in to reply to this topic.