ColorPicker.psm1

<#
.SYNOPSIS
    Simple colorpicker
 
.DESCRIPTION
    Simple colorpicker made using Windows Forms in PowerShell. Select pixel on screen and get HEX + RGB color value.
 
.AUTHOR
    EW
 
.COPYRIGHT
    No
 
.LICENSE
    None
 
.VERSION
    0.0.7
 
.NOTES
    ...
 
.EXAMPLE
    Get-Color
     
#>


New-Alias -Name Color -Value Get-Color



Function Get-Color {
    Start-Job -ScriptBlock {
        Add-Type -AssemblyName System.Windows.Forms
        Add-Type -AssemblyName System.Drawing

        # Helper to poll mouse button state without blocking the UI
        $CSharpGetAsyncKeyState = @"
using System;
using System.Runtime.InteropServices;
public static class Native {
  [DllImport("user32.dll")] public static extern short GetAsyncKeyState(int vKey);
}
"@


        Add-Type -Language CSharp -TypeDefinition $CSharpGetAsyncKeyState

        Function Get-PixelColorAtCursor {
            $Position = [System.Windows.Forms.Cursor]::Position
            $Bitmap = New-Object System.Drawing.Bitmap 1,1
            $Graphics = [System.Drawing.Graphics]::FromImage($Bitmap)
            $Graphics.CopyFromScreen($Position, [System.Drawing.Point]::Empty, $Bitmap.Size)
            $Graphics.Dispose()
            $C = $Bitmap.GetPixel(0,0)
            $Bitmap.Dispose()
            $C
        }

        $Form = New-Object System.Windows.Forms.Form
        $Form.Text = 'Doubleclick values to copy'
        $Form.StartPosition = 'CenterScreen'
        $Form.Size = New-Object System.Drawing.Size(380,180)
        $Form.FormBorderStyle = 'FixedDialog'
        $Form.MaximizeBox = $false
        $Form.KeyPreview = $true

        $Preview = New-Object System.Windows.Forms.Panel
        $Preview.Size = New-Object System.Drawing.Size(80,80)
        $Preview.Location = New-Object System.Drawing.Point(20,20)
        $Preview.BorderStyle = 'FixedSingle'
        $Preview.Add_Click({            
            $ValueHex.Text | Set-Clipboard
            [System.Windows.Forms.MessageBox]::Show(
                "Copied '$($ValueHex.Text)' to clipboard",
                "Copied",
                [System.Windows.Forms.MessageBoxButtons]::OK,
                [System.Windows.Forms.MessageBoxIcon]::Information
            ) | Out-Null
        })       
        $Form.Controls.Add($Preview)

        $LabelHex = New-Object System.Windows.Forms.Label
        $LabelHex.AutoSize = $true
        $LabelHex.Location = New-Object System.Drawing.Point(120,30)
        $LabelHex.Text = 'HEX:'
        $Form.Controls.Add($LabelHex)

        $ValueHex = New-Object System.Windows.Forms.Label
        $ValueHex.AutoSize = $true
        $ValueHex.Location = New-Object System.Drawing.Point(160,30)
        $ValueHex.Add_MouseEnter({ $ValueHex.ForeColor = [System.Drawing.Color]::FromArgb(100,100,100) })
        $ValueHex.Add_MouseLeave({ $ValueHex.ForeColor = [System.Drawing.Color]::FromArgb(0,0,0) })
        $ValueHex.Add_Click({            
            $ValueHex.Text | Set-Clipboard
            [System.Windows.Forms.MessageBox]::Show(
                "Copied '$($ValueHex.Text)' to clipboard",
                "Copied",
                [System.Windows.Forms.MessageBoxButtons]::OK,
                [System.Windows.Forms.MessageBoxIcon]::Information
            ) | Out-Null
        })
        $Form.Controls.Add($ValueHex)
        
        $LabelRGB = New-Object System.Windows.Forms.Label
        $LabelRGB.AutoSize = $true
        $LabelRGB.Location = New-Object System.Drawing.Point(120,60)
        $LabelRGB.Text = 'RGB:'
        $Form.Controls.Add($LabelRGB)

        $ValueRGB = New-Object System.Windows.Forms.Label
        $ValueRGB.AutoSize = $true
        $ValueRGB.Location = New-Object System.Drawing.Point(160,60)
        $ValueRGB.Add_MouseEnter({ $ValueRGB.ForeColor = [System.Drawing.Color]::FromArgb(100,100,100) })
        $ValueRGB.Add_MouseLeave({ $ValueRGB.ForeColor = [System.Drawing.Color]::FromArgb(0,0,0) })
        $ValueRGB.Add_Click({            
            $ValueRGB.Text | Set-Clipboard            
            [System.Windows.Forms.MessageBox]::Show(
                "Copied '$($ValueRGB.Text)' to clipboard",
                "Copied",
                [System.Windows.Forms.MessageBoxButtons]::OK,
                [System.Windows.Forms.MessageBoxIcon]::Information
            ) | Out-Null
        })
        $Form.Controls.Add($ValueRGB)

        $Button_ColorDialog = New-Object System.Windows.Forms.Button
        $Button_ColorDialog.Text = "Colorpicker"
        $Button_ColorDialog.Size = New-Object System.Drawing.Size(110,30)
        $Button_ColorDialog.Location = New-Object System.Drawing.Point(120,100)
        $Form.Controls.Add($Button_ColorDialog)

        $Button_SampleScreen = New-Object System.Windows.Forms.Button
        $Button_SampleScreen.Text = "Sample screen"
        $Button_SampleScreen.Size = New-Object System.Drawing.Size(110,30)
        $Button_SampleScreen.Location = New-Object System.Drawing.Point(240,100)
        $Form.Controls.Add($Button_SampleScreen)

        # Helpers to show a color
        $ShowColor = {
            param([System.Drawing.Color]$C)
            $Preview.BackColor = $C
            $ValueHex.Text  = ("#{0:X2}{1:X2}{2:X2}" -f $C.R,$C.G,$C.B)
            $ValueRGB.Text = ("{0},{1},{2}" -f $C.R,$C.G,$C.B)
            $ValueHex.Text | Set-Clipboard
        }

        # Show system Color dialog. Set preview color and add hex to clipboard when clicking ok.
        $Button_ColorDialog.Add_Click({

            $ColorDialog = New-Object System.Windows.Forms.ColorDialog
            $ColorDialog.FullOpen = $true
            $ColorDialog.AnyColor = $true

            if ($ColorDialog.ShowDialog() -eq 'OK') {
                & $ShowColor $ColorDialog.Color
                $ValueHex.Text | Set-Clipboard
                $ColorDialog.Dispose()
            }

        })

        # Eyedropper triggers when clicking the "Sample Screen" button
        $Button_SampleScreen.Add_Click({
            
            # Parameters
            $SampleSize = 30  # 30x30 pixels around the cursor
            $LoupeScale = 4                  
            $LoupeSize  = $SampleSize * $LoupeScale  # 120x120 magnify view
            $LoupeOffset = New-Object System.Drawing.Point(24,24)  # Move the magnify preview away from the mouse pointer

            # Create the magnify window
            $Loupe = New-Object System.Windows.Forms.Form
            $Loupe.FormBorderStyle = 'None'
            $Loupe.ShowInTaskbar = $false
            $Loupe.TopMost = $true
            $Loupe.StartPosition = 'Manual'
            $Loupe.Size = New-Object System.Drawing.Size($LoupeSize, $LoupeSize)
            $Loupe.BackColor = [System.Drawing.Color]::Black  # just in case we see behind image
            $Loupe.Opacity = 0.98

            # This will contain the magnify image
            $LoupeImg = $null

            # Timer used to update position + bitmap from background
            $Timer = New-Object System.Windows.Forms.Timer
            $Timer.Interval = 16   # ~60 FPS

            # Pre-create bitmaps to avoid allocations on every tick (60 times per second)
            $PixelFormat = [System.Drawing.Imaging.PixelFormat]::Format32bppArgb    
            $SrcBmp  = New-Object System.Drawing.Bitmap ($SampleSize, $SampleSize, $PixelFormat)
            $DstBmp  = New-Object System.Drawing.Bitmap ($LoupeSize, $LoupeSize, $PixelFormat)

            # Screen bounds (multi-monitor aware)
            $VirtualScreen = [System.Windows.Forms.SystemInformation]::VirtualScreen

            #Logic that runs over and over when picker is active:
            $UpdateLoupe = {
                # Where's the pointer?
                $Position = [System.Windows.Forms.Cursor]::Position

                # Keep the 50x50 capture fully on-screen
                $SrcX = [Math]::Max($VirtualScreen.Left,  [Math]::Min($Position.X - [int]($SampleSize/2), $VirtualScreen.Right  - $SampleSize))
                $SrcY = [Math]::Max($VirtualScreen.Top,   [Math]::Min($Position.Y - [int]($SampleSize/2), $VirtualScreen.Bottom - $SampleSize))

                # Capture 50x50 around cursor
                $GraphicsSrc = [System.Drawing.Graphics]::FromImage($SrcBmp)
                $GraphicsSrc.CopyFromScreen($SrcX, $SrcY, 0, 0, $SrcBmp.Size)
                $GraphicsSrc.Dispose()

                # Scale up to the loupe size (nearest-neighbor for crisp pixels)
                $GraphicsDst = [System.Drawing.Graphics]::FromImage($dstBmp)
                $GraphicsDst.CompositingMode  = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy
                $GraphicsDst.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighSpeed
                $GraphicsDst.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::NearestNeighbor
                $GraphicsDst.PixelOffsetMode  = [System.Drawing.Drawing2D.PixelOffsetMode]::Half
                $GraphicsDst.SmoothingMode    = [System.Drawing.Drawing2D.SmoothingMode]::None
                $GraphicsDst.Clear([System.Drawing.Color]::Black)
                $Rectangle = New-Object System.Drawing.Rectangle(0,0,$LoupeSize,$LoupeSize)
                $GraphicsDst.DrawImage($SrcBmp, $Rectangle)

                # Draw a crosshair in center of the loupe
                $Pen1 = New-Object System.Drawing.Pen ([System.Drawing.Color]::White), 1
                $Pen2 = New-Object System.Drawing.Pen ([System.Drawing.Color]::Black), 1
                $CX = [int]($LoupeSize/2)
                $CY = [int]($LoupeSize/2)

                # First white, then black (contrast)
                $GraphicsDst.DrawLine($Pen1, $CX, 0, $CX, $LoupeSize)
                $GraphicsDst.DrawLine($Pen1, 0, $CY, $LoupeSize, $CY)
                $GraphicsDst.DrawLine($Pen2, $CX+1, 0, $CX+1, $LoupeSize)
                $GraphicsDst.DrawLine($Pen2, 0, $CY+1, $LoupeSize, $CY+1)        
                $Pen1.Dispose()
                $Pen2.Dispose()
                $GraphicsDst.Dispose()

                # Swap into the loupe form
                if ($LoupeImg) { $Loupe.BackgroundImage.Dispose() }
                $LoupeImg = [System.Drawing.Bitmap]$dstBmp.Clone()
                $Loupe.BackgroundImage = $LoupeImg
                $Loupe.BackgroundImageLayout = 'Stretch'

                # Position the loupe near the cursor (clamp on-screen)
                $LocationX = $Position.X + $LoupeOffset.X
                $LocationY = $Position.Y + $LoupeOffset.Y
                if ($LocationX + $Loupe.Width  -gt $VirtualScreen.Right)  { $LocationX = $Position.X - $LoupeOffset.X - $Loupe.Width }
                if ($LocationY + $Loupe.Height -gt $VirtualScreen.Bottom) { $LocationY = $Position.Y - $LoupeOffset.Y - $Loupe.Height }
                if ($LocationX -lt $VirtualScreen.Left)   { $LocationX = $VirtualScreen.Left }
                if ($LocationY -lt $VirtualScreen.Top)    { $LocationY = $VirtualScreen.Top  }
                $Loupe.Location = New-Object System.Drawing.Point($LocationX, $LocationY)
            }

            $TickHandler = {
                # Keep UI awake
                [System.Windows.Forms.Application]::DoEvents()
                & $UpdateLoupe
            }

            try {
                # Remember and hide the picker window so it doesn't block the pixel under cursor
                $WasTopMost = $Form.TopMost
                $Form.TopMost = $false
                $Form.Hide()
                Start-Sleep -Milliseconds 60

                # Show loupe and start updating
                $Loupe.Show()
                $Timer.Add_Tick($TickHandler)
                $Timer.Start()

                # Crosshair cursor while picking
                [System.Windows.Forms.Cursor]::Current = [System.Windows.Forms.Cursors]::Cross

                # Wait for next *press* of the left mouse button (VK_LBUTTON = 0x01)
                while ($true) {
                    [System.Windows.Forms.Application]::DoEvents()
                    Start-Sleep -Milliseconds 10

                    $State = [Native]::GetAsyncKeyState(0x01)
                    if (($State -band 0x8000) -ne 0) {
                        while(([Native]::GetAsyncKeyState(0x01) -band 0x8000) -ne 0) {
                            Start-Sleep -Milliseconds 5
                        }
                        break
                    }

                    # Allow cancel with Esc
                    if ([Native]::GetAsyncKeyState(0x1B) -band 0x8000) {
                        throw 'Canceled'
                    }
                }

                # Final read uses your existing helper
                $C = Get-PixelColorAtCursor
                & $ShowColor $C
            }
            catch {
                #
            }
            finally {
                # Stop timer and clean up loupe resources
                $Timer.Stop()
                $Timer.Remove_Tick($TickHandler)
                $Timer.Dispose()

                if ($LoupeImg) {
                    $Loupe.BackgroundImage.Dispose()
                    $LoupeImg.Dispose()
                }

                $Loupe.Close()
                $Loupe.Dispose()
                $SrcBmp.Dispose()
                $DstBmp.Dispose()

                # Restore cursor and main form
                [System.Windows.Forms.Cursor]::Current = [System.Windows.Forms.Cursors]::Default
                $Form.Show()
                $Form.TopMost = $WasTopMost
                $Form.Activate()
            }
        })

        


        # Allow closing form with escape key
        $Form.Add_KeyDown({
            if ($_.KeyCode -eq 'Escape') {
                $Form.Close()
            }
        })

        [System.Windows.Forms.Application]::EnableVisualStyles()
        & $ShowColor ([System.Drawing.Color]::FromArgb(255, 32, 32, 32))
        [void]$Form.ShowDialog()
    } | Out-Null
}