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.3
 
.NOTES
    ...
 
.EXAMPLE
    Get-Color
     
#>



# Helper to poll mouse button state without blocking the UI

New-Alias -Name Color -Value Get-Color

$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-Color {
    Start-ThreadJob -Name "ColorPicker" -ScriptBlock {
        Add-Type -AssemblyName System.Windows.Forms
        Add-Type -AssemblyName System.Drawing

        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 = 'Color Picker'
        $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'
        $Form.Controls.Add($Preview)

        $Label_Hex = New-Object System.Windows.Forms.Label
        $Label_Hex.AutoSize = $true
        $Label_Hex.Location = New-Object System.Drawing.Point(120,30)
        $Label_Hex.Add_MouseEnter({ $Label_Hex.ForeColor = [System.Drawing.Color]::FromArgb(100,100,100) })
        $Label_Hex.Add_MouseLeave({ $Label_Hex.ForeColor = [System.Drawing.Color]::FromArgb(0,0,0) })
        $Label_Hex.Add_Click({
            $ClipB = $Label_Hex.Text -replace "^HEX: "
            if ($ClipB) {
                [System.Windows.Forms.Clipboard]::SetText($ClipB)
            }
            $Label_Hex.ForeColor = [System.Drawing.Color]::FromArgb(255,255,255)
            Start-Sleep -Seconds 0.2    
            $Label_Hex.ForeColor = [System.Drawing.Color]::FromArgb(100,100,100)
        })
        $Form.Controls.Add($Label_Hex)

        $Label_RGB = New-Object System.Windows.Forms.Label
        $Label_RGB.AutoSize = $true
        $Label_RGB.Location = New-Object System.Drawing.Point(120,60)
        $Label_RGB.Add_MouseEnter({ $Label_RGB.ForeColor = [System.Drawing.Color]::FromArgb(100,100,100) })
        $Label_RGB.Add_MouseLeave({ $Label_RGB.ForeColor = [System.Drawing.Color]::FromArgb(0,0,0) })
        $Label_RGB.Add_Click({
            $ClipB = $Label_RGB.Text -replace "^RGB: " -replace "\w=" -replace "\s", ','
            if ($ClipB) {
                [System.Windows.Forms.Clipboard]::SetText($ClipB)
            }
            $Label_RGB.ForeColor = [System.Drawing.Color]::FromArgb(255,255,255)
            Start-Sleep -Seconds 0.2    
            $Label_RGB.ForeColor = [System.Drawing.Color]::FromArgb(100,100,100)
        })
        $Form.Controls.Add($Label_RGB)

        $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
            $Label_Hex.Text  = "HEX: " + ("#{0:X2}{1:X2}{2:X2}" -f $C.R,$C.G,$C.B)
            $Label_RGB.Text = "RGB: " + ("R={0} G={1} B={2}" -f $C.R,$C.G,$C.B)
        }

        # 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
                $ClipB = $Label_Hex.Text -replace "^HEX: "
                if ($ClipB) {
                    [System.Windows.Forms.Clipboard]::SetText($ClipB)
                }
                $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 {
                if ($_.Exception.Message -eq 'Canceled') {
                    $Label_Hex.Text = 'Pick canceled'
                    $Label_RGB.Text = ''
                }
            }
            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()
            }
        })

        # Copy HEX to clipboard if preview color is clicked
        $Preview.Add_Click({
            $ClipB = $Label_Hex.Text -replace "^HEX: "
            if ($ClipB) {
                [System.Windows.Forms.Clipboard]::SetText($ClipB)
            }
        })


        # 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
}