PowerShell Form Menu: 스크립트에 빠르게 액세스하기

다시 한 번 주말 프로젝트 시간이다. 오늘은 가장 소중한 PowerShell 스크립트를 빠르고 쉽게 실행할 수 있는 경량 시스템 트레이 PowerShell 폼 메뉴를 만드는 방법을 배우게 될 것이다. 아래에서 최종 결과를 볼 수 있다.

Launching PowerShell scripts via a system tray menu icon

이 문서에서는 프로세스를 단계별로 나누어 자체 PowerShell 메뉴 GUI를 만드는 방법을 배울 것이다.

환경 및 지식 요구 사항

시작하기 전에 다음 최소 요구 사항을 충족하는지 확인해야 한다.

  • Windows 7 이상
  • Windows PowerShell 3 이상 – 최신 버전의 .NET Core 3.0과 PowerShell 7 미리 보기는 WPF 및 WinForm에 대한 최근 추가된 지원으로 인해 Windows에서 작동할 수 있지만 테스트되지 않았다.
  • .NET Framework 4.5 이상
  • A familiarity with Windows Forms (WinForms)  You can, however, due this with WPF too though.

이 프로젝트에서 좋은 소식은 주로 다음 구성 요소에 의존해야 하므로 Visual Studio, PoshGUI 또는 다른 UI 개발 도구에 실제로 의존할 필요가 없다는 것이다:

  • NotifyIcon – 사용자가 상호 작용할 수 있는 사용자 정의 시스템 트레이 아이콘으로 사용된다.
  • ContextMenu – 사용자가 시스템 트레이 아이콘을 우클릭했을 때의 컨테이너입니다.
  • MenuItem – 우클릭 메뉴 내 각 옵션의 개별 객체입니다.

좋아하는 PowerShell 스크립트 편집기를 열고 시작해 봅시다!

이 프로젝트에서는 세 가지 함수를 만들 것입니다: 콘솔을 숨기거나 표시하여 더 깔끔한 사용자 경험을 제공하는 두 개의 함수 및 시스템 트레이 메뉴에 항목을 추가하는 함수입니다. 이러한 함수들은 이후에 훨씬 쉬운 삶을 위한 기반이 될 것이며, 이 글의 후반부에서 자세히 배우게 될 것입니다.

콘솔 창 표시/숨기기

숨겨지지 않은 경우, PowerShell 스크립트를 실행하면 익숙한 PowerShell 콘솔이 나타날 것입니다. 만들 예정인 PowerShell 폼의 메뉴 항목들은 스크립트를 실행하는 것이기 때문에 콘솔이 나타나지 않도록 해야 합니다. 실행만 되면 됩니다.

스크립트가 실행될 때, 작은 .NET을 사용하여 PowerShell 콘솔 창을 표시하거나 표시하지 않을 수 있습니다.

현재 세션에 Window .NET 유형을 추가하십시오. 이를 위해 아래에 표시된대로 C#을 사용할 것입니다. 현재 PowerShell 스크립트의 컨텍스트에 로드해야하는 두 가지 메서드는 GetConsoleWindowShowWindow입니다. 이 DLL을 메모리에 로드함으로써 API의 특정 부분을 노출시킬 수 있으며, 이를 사용하여 PowerShell 스크립트의 컨텍스트에서 이들을 사용할 수 있습니다:

 #현재 콘솔 세션의 컨텍스트에 DLL 로드
 Add-Type -Name Window -Namespace Console -MemberDefinition '
    [DllImport("Kernel32.dll")]
    public static extern IntPtr GetConsoleWindow();
 
    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);

위에서 로드한 것을 사용하여 GetConsoleWindow()ShowWindow() 메서드를 사용하여 두 개의 함수를 생성하십시오.

 function Start-ShowConsole {
    $PSConsole = [Console.Window]::GetConsoleWindow()
    [Console.Window]::ShowWindow($PSConsole, 5)
 }
 
 function Start-HideConsole {
    $PSConsole = [Console.Window]::GetConsoleWindow()
    [Console.Window]::ShowWindow($PSConsole, 0)
 }

이 두 함수를 사용하여 콘솔 창을 원하는대로 표시하거나 숨길 수 있는 방법을 만들었습니다.

참고: 메뉴를 통해 실행 된 스크립트의 출력을 보려면 PowerShell 트랜스크립트 또는 기타 텍스트 기반의 로깅 기능을 사용할 수 있습니다. 이로써 PowerShell 세션을 WindowStyle 매개 변수를 사용하여 숨기는 것만으로 실행하는 것보다 제어를 유지할 수 있습니다.

이제 Start-HideConsole을 호출하여 스크립트 코드를 작성하기 시작하십시오. PowerShell 폼 메뉴 기반 스크립트가 실행되면 PowerShell 콘솔 창이 표시되지 않도록 보장합니다.

<# 
	기능 및 객체 초기화
	텍스트 기반의 로딩 막대 표시 또는 Write-Progress를 호스트에 표시
#>
 
Start-HideConsole
 
<# 
	폼/시스템 트레이 아이콘을 표시하는 코드
	이 부분에서 콘솔을 유지하고 닫을 때까지 대기합니다.
#>

메뉴 옵션 만들기

이제 메뉴 옵션을 만들 차례입니다. 나중에 쉽게 새로운 옵션을 만들 수 있도록, 이번에는 New-MenuItem라는 함수를 만듭니다. 이 함수를 호출하면, 메뉴에 추가할 수 있는 새로운 MenuItem .NET 객체가 생성됩니다.

각 메뉴 옵션은 다른 스크립트를 실행하거나 런처를 종료합니다. 이 기능을 수용하기 위해, New-MenuItem 함수에는 세 개의 매개변수가 있습니다:

  • Text – 사용자가 클릭할 레이블
  • MyScriptPath – 실행할 PowerShell 스크립트의 경로
  • ExitOnly – 런처를 종료할지 여부

아래 함수 스니펫을 메뉴 스크립트에 추가하세요.

 function New-MenuItem{
     param(
         [string]
         $Text = "Placeholder Text",
 
         $MyScriptPath,
         
         [switch]
         $ExitOnly = $false
     )
 #초기화
 $MenuItem = New-Object System.Windows.Forms.MenuItem

다음으로, 메뉴 아이템에 텍스트 레이블을 할당합니다.

 #원하는 텍스트 적용
 if($Text) {
 	$MenuItem.Text = $Text
 }

이제 MenuItem에 MyScriptPath라는 사용자 정의 속성을 추가하세요. 이 경로는 메뉴에서 항목을 클릭할 때 호출됩니다.

 #클릭 이벤트 로직 적용
 if($MyScriptPath -and !$ExitOnly){
 	$MenuItem | Add-Member -Name MyScriptPath -Value $MyScriptPath -MemberType NoteProperty

원하는 스크립트를 실행하는 MenuItem에 클릭 이벤트를 추가하십시오. Start-Process는 try/catch 블록 내에서 깔끔하게 이 작업을 수행할 수 있도록하며, PowerShell의 사용 불가 또는 제공된 경로에 스크립트가 없는 경우와 같은 스크립트 실행 중 발생하는 모든 오류를 catch 블록으로 이동시킬 수 있도록합니다.

   $MenuItem.Add_Click({
        try{
            $MyScriptPath = $This.MyScriptPath #클릭 이벤트 중 올바른 경로 찾기에 사용됩니다
            
            if(Test-Path $MyScriptPath){
                Start-Process -FilePath "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList "-NoProfile -NoLogo -ExecutionPolicy Bypass -File `"$MyScriptPath`"" -ErrorAction Stop
            } else {
                throw "Could not find at path: $MyScriptPath"
            }
        } catch {
          $Text = $This.Text
          [System.Windows.Forms.MessageBox]::Show("Failed to launch $Text`n`n$_") > $null
        }
  })

런처에 종료 조건을 제공하고, 새로 만든 MenuItem을 실행시킬 변수에 다시 할당하기 위해 남은 로직을 추가하십시오.

    #런처 종료 방법 제공
    if($ExitOnly -and !$MyScriptPath){
        $MenuItem.Add_Click({
            $Form.Close()
    
            #멈춘 프로세스 처리
            Stop-Process $PID
        })
    }
 
 	 #새로운 MenuItem 반환
    $MenuItem
 }

이제 New-MenuItem 함수가 생성되었습니다! 최종 함수는 다음과 같아야합니다:

 function New-MenuItem{
     param(
         [string]
         $Text = "Placeholder Text",
 
         $MyScriptPath,
         
         [switch]
         $ExitOnly = $false
     )
 
     #초기화
     $MenuItem = New-Object System.Windows.Forms.MenuItem
 
     #원하는 텍스트 적용
     if($Text){
         $MenuItem.Text = $Text
     }
 
     #클릭 이벤트 로직 적용
     if($MyScriptPath -and !$ExitOnly){
         $MenuItem | Add-Member -Name MyScriptPath -Value $MyScriptPath -MemberType NoteProperty
     }
 
     $MenuItem.Add_Click({
             try{
                 $MyScriptPath = $This.MyScriptPath #클릭 이벤트 중 적절한 경로 찾기에 사용됨
             
                 if(Test-Path $MyScriptPath){
                     Start-Process -FilePath "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList "-NoProfile -NoLogo -ExecutionPolicy Bypass -File `"$MyScriptPath`"" -ErrorAction Stop
                 } else {
                     throw "Could not find at path: $MyScriptPath"
                 }
             } catch {
                 $Text = $This.Text
                 [System.Windows.Forms.MessageBox]::Show("Failed to launch $Text`n`n$_") > $null
             }
         })
 
     #런처를 종료하는 방법 제공
     if($ExitOnly -and !$MyScriptPath){
         $MenuItem.Add_Click({
                 $Form.Close()
    
                 #멈춘 프로세스 처리
                 Stop-Process $PID
             })
     }
 
     #새로운 MenuItem 반환
     $MenuItem
 }

위의 코드를 PowerShell 콘솔에 복사하여 붙여넣고 가짜 매개변수 값을 제공하며 함수를 실행하여 New-MenuItem 함수를 테스트하세요. .NET MenuItem 객체가 반환되는 것을 볼 수 있을 것입니다.

 PS51> (New-MenuItem -Text "Test" -MyScriptPath "C:\test.ps1").GetType()
 
 IsPublic IsSerial Name                                     BaseType
 -------- -------- ----                                     --------
 True     False    MenuItem                                 System.Windows.Forms.Menu

런처 폼 생성

이와 같은 팁을 더 원하신다면, 제 개인 PowerShell 블로그를 방문해보세요: https://nkasco.com/FriendsOfATA

이제 새로운 메뉴 항목을 쉽게 생성할 수 있으므로 메뉴를 표시할 시스템 트레이 런처를 생성할 차례입니다.

구성 요소를 추가할 기본적인 폼 개체를 생성하세요. 이는 사용자에게 표시되지 않고 콘솔이 백그라운드에서 실행되도록 유지될 것이므로 특별히 꾸미지 않아도 됩니다.

 #컴포넌트를 담을 컨테이너로 사용할 폼 생성
 $Form = New-Object System.Windows.Forms.Form
 ​
 #폼을 숨기도록 구성
 $Form.BackColor = "Magenta" #이 색상을 폼의 TransparencyKey 속성과 일치시켜 폼의 투명도 설정
 $Form.TransparencyKey = "Magenta"
 $Form.ShowInTaskbar = $false
 $Form.FormBorderStyle = "None"

다음으로 시스템 트레이에 표시될 아이콘을 생성합니다. 아래에서는 PowerShell 아이콘을 사용하도록 선택했습니다. 실행 중에 아래 코드는 실제 시스템 트레이 아이콘을 생성합니다. 이 아이콘은 SystrayIcon 변수를 설정하여 원하는 아이콘으로 사용자 정의할 수 있습니다.

기타 아이콘을 메모리에 로드하는 방법을 확인하려면 System.Drawing.Icon 클래스의 문서를 참조하십시오.

 #필요한 구성 요소 초기화/설정
 $SystrayLauncher = New-Object System.Windows.Forms.NotifyIcon
 $SystrayIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe")
 $SystrayLauncher.Icon = $SystrayIcon
 $SystrayLauncher.Text = "PowerShell Launcher"
 $SystrayLauncher.Visible = $true

스크립트를 실행하면 아래와 같이 PowerShell 아이콘이 시스템 트레이에 표시됩니다.

이제 새 ContextMenu 객체를 사용하여 메뉴 항목을 담을 컨테이너를 생성하고 모든 메뉴 항목을 생성합니다. 이 예제에서는 두 개의 스크립트 실행 옵션과 종료 옵션이 있는 메뉴가 있습니다.

 $ContextMenu = New-Object System.Windows.Forms.ContextMenu
 ​
 $LoggedOnUser = New-MenuItem -Text "Get Logged On User" -MyScriptPath "C:\scripts\GetLoggedOn.ps1"
 $RestartRemoteComputer = New-MenuItem -Text "Restart Remote PC" -MyScriptPath "C:\scripts\restartpc.ps1"
 $ExitLauncher = New-MenuItem -Text "Exit" -ExitOnly

그런 다음, 방금 생성한 모든 메뉴 항목을 컨텍스트 메뉴에 추가합니다. 이렇게 하면 각 메뉴 옵션이 폼 컨텍스트 메뉴에 표시됩니다.

 #메뉴 항목을 컨텍스트 메뉴에 추가
 $ContextMenu.MenuItems.AddRange($LoggedOnUser)
 $ContextMenu.MenuItems.AddRange($RestartRemoteComputer)
 $ContextMenu.MenuItems.AddRange($ExitLauncher)#구성 요소를 폼에 추가
 $SystrayLauncher.ContextMenu = $ContextMenu

런처 폼 표시

이제 폼이 완성되었으므로 마지막으로 PowerShell 콘솔 창이 나타나지 않도록 폼을 표시해야 합니다. Start-HideConsole을 사용하여 런처 폼을 표시한 후, Start-ShowConsole을 사용하여 콘솔을 다시 표시하여 powershell.exe 프로세스가 중단되지 않도록 합니다.

#시작
Start-HideConsole
$Form.ShowDialog() > $null
Start-ShowConsole

이와 같은 팁을 더 원하시나요? 제 개인 PowerShell 블로그를 확인해보세요: https://nkasco.com/FriendsOfATA

전체 코드는 여기에서 찾을 수 있습니다: https://github.com/nkasco/PSSystrayLauncher

중요 사항

축하합니다. 이 프로젝트를 완료했습니다! 이 글에서 다음을 배웠습니다:

  1. Windows API의 구성 요소를 노출하는 방법.
  2. WinForms를 통해 컨텍스트 메뉴를 사용하고 이후 메뉴 항목을 추가하는 방법.
  3. PowerShell에서 시스템 트레이 아이콘을 만드는 방법.

이 프로젝트를 통해 PowerShell 스크립트용 자체 시스템 트레이 메뉴를 만들기에 충분한 이해와 경험을 얻을 수 있을 것입니다!

Source:
https://adamtheautomator.com/powershell-form/