Let's continue looking at how to use PowerShell and a Windows Presentation Foundation (WPF) form to display [System.Drawing.Color] values. This article builds on an earlier post so if you missed it, take a few minutes to get caught up. As I did earlier, before running any WPF code in PowerShell, you should load the required type assemblies.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Add-Type -AssemblyName PresentationFramework
I should point out that this isn't an absolute requirement since some hosts like the PowerShell ISE will run WPF code without it. But you never know where your code is going to be executed, so it never hurts to run the Add-Type command.
Multiple Color Display
I'm going to continue building on my earlier code. I like working incrementally. This allows me to identify potential obstacles and test techniques. My interim code should enable me to display multiple colors. Because this is a development function, I'll limit the number of colors to test to 10. I can use a ValidateCount() test for the parameter value.
Param(
[Parameter(
Position = 0,
Mandatory,
HelpMessage = "Enter between 1 and 10 System.Drawing.Color names"
)]
[ValidateCount(1, 10)]
[ValidatePattern("^\w+$")]
[string[]]$Color
)
I am also adding a regex pattern test to ensure the color value is a simple string with no spaces. Yes, I realize I could improve this pattern, but it will suffice for my proof-of-concept needs.
Nothing changes with the code to build the WPF form. For this test, I'll also use the StackPanel and add each color to the stack.
foreach ($item in $color) {
$sdc = [System.Drawing.Color]::FromName($item)
#Validate the color
if ($sdc.IsKnownColor) {
# <create a textbox for each color>
# <convert color to html value>
# <add the textbox to the stack>
}
else {
Write-Warning "Cannot find $item as a System.Drawing.Color"
}
}
As I was working on this, I realized I needed to handle situations where I entered an incorrect color. The FromName() method will always work, even with a value like 'foo'. But in that case, the IsKnownColor value will be False. Here's the complete sample function.
Function Show-SelectedColor {
# sample usage: Show-SelectedColor green,chartreuse,yellow,violet,darkorange
[CmdletBinding()]
Param(
[Parameter(
Position = 0,
Mandatory,
HelpMessage = "Enter between 1 and 10 System.Drawing.Color names"
)]
[ValidateCount(1, 10)]
[ValidatePattern("^\w+$")]
[string[]]$Color
)
$form = New-Object System.Windows.Window
$form.Title = "Color Sample"
$form.Height = 100
$form.Width = 300
$form.top = 200
$form.left = 100
$form.UseLayoutRounding = $True
$form.WindowStartupLocation = "CenterScreen"
$form.SizeToContent = "Height"
$stack = New-Object System.Windows.Controls.StackPanel
foreach ($item in $color) {
$sdc = [System.Drawing.Color]::FromName($item)
#Validate the color
if ($sdc.IsKnownColor) {
#create a textbox for each color
$TextBox = New-Object System.Windows.Controls.TextBox
$TextBox.Width = 275
$textbox.Height = 30
$TextBox.HorizontalAlignment = "Center"
$textbox.TextAlignment = "center"
$textbox.VerticalContentAlignment = "center"
$textbox.FontSize = 16
#The drawing color may need to be converted to a brush color
#convert color to html value
$htmlcode = "#{0:X2}{1:X}{2:X2}" -f $sdc.r, $sdc.g, $sdc.b
$textbox.background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($htmlcode)
$TextBox.Text = "$($sdc.name) ($($sdc.R),$($sdc.G),$($sdc.B))"
#add the textbox to the stack
$stack.AddChild($TextBox)
}
else {
Write-Warning "Cannot find $item as a System.Drawing.Color"
}
}
$form.AddChild($stack)
[void]$form.ShowDialog()
}
I now have a general structure for my final project.
Adding a WPF Grid
While using the StackPanel is easy, I knew I didn't want to have a stack of 134 layers. I envisioned multiple rows of 3 columns each. But, I also wanted this to be dynamic. I wanted the form to adjust to the number of specified colors automatically. I also intend for the function to accept wildcards and regex patterns, because why not?!
Now things get tricky. For this type of layout, I need to use a Grid control instead of a StackPanel.
$grid = New-Object system.windows.Controls.Grid
Within the grid, I need to define rows and columns. I can calculate the maximum number of rows I'll need by dividing the number of colors by 3. However, I need to consider edge cases where I might have 2 or 4 colors.
switch ($data.count ) {
{ $_ -eq 0 } { Write-Warning "No color values found." ; Return }
{ $_ -le 2 } { $maxRows = 1 ; Break }
{ $_ -le 5 } { $maxRows = 2 ; Break }
Default { $maxRows = $data.count / 3 -as [int] }
}
With this information, I can create rows and columns and add them to the grid.
for ($i = 0; $i -lt $maxRows; $i++) {
$row = New-Object System.Windows.Controls.RowDefinition
$row.Height = 30
$grid.RowDefinitions.add($row)
}
for ($i = 0; $i -lt 3; $i++) {
$col = New-Object System.Windows.Controls.ColumnDefinition
$col.Width = 300
$grid.ColumnDefinitions.Add($col)
}
I can use the same type of ForEach enumeration to create a text box for each color. But now, I hit an obstacle. I need to position each text box in a row and column combination. When using XAML, the layout is handled like this.
<TextBox x:Name="textComputername" Grid.Column="1" HorizontalAlignment="Left" Height="20"
Margin="20,3,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="170" ToolTip="Enter a
computername or a comma separated list" TabIndex="0"/>
In this XAML snippet from another project, the TextBox is placed in grid column 1. But I'm not using XAML.
I know I'll need to keep track of my position, so I'll initialize some counters for the row and column.
$r = 0 #row index
$c = 0 #column index
Unfortunately, it isn't as simple as $textbox.grid.column.row = $r. Instead, this relies on a WPF construct known as a DependencyProperty. WPF controls have a SetValue() method that accomplish this task.
This took quite a bit of research, headbanging, and minor curse words to get my head wrapped around. Eventually, I found some code from the ShowUI module, which helped me understand. The dependency properties will be $grid.row and $grid.column. Although, I don't reference my variables, like $grid. Instead, I use a reference to the Grid class and the desired property.
As I create each textbox, I can "place" it in the correct row and column.
$textbox.SetValue([system.windows.controls.grid]::rowproperty, $r)
$textbox.SetValue([system.windows.controls.grid]::columnproperty, $c)
The last bit of row and column management is to keep track of and move to the next row after three columns.
$c++
$grid.AddChild($TextBox)
if ($c -eq 3) {
$c = 0
$r++
}
Scrolling
Next, I needed to consider how the form would be displayed. I didn't want a huge form. After some testing, I realized that if there were fifteen for fewer rows, I could have the form auto-size itself.
if ($maxRows -le 15) {
$form.SizeToContent = "Height"
}
Otherwise, I wanted a defined window size and the ability to scroll the contents. But this isn't as simple as enabling scroll bars on the form. Think of WPF as a set of Russian nesting dolls. There is a separate control for scrolling.
$scroll = New-Object System.Windows.Controls.ScrollViewer
$scroll.VerticalScrollBarVisibility = "Visible"
$scroll.HorizontalScrollBarVisibility = "Auto"
Once the grid is complete, I can add it to the ScrollViwer and then add the ScrollViewer to the form.
$scroll.AddChild($grid)
$form.AddChild($scroll)
Processing Names
The last piece of my code dealt with color names. I wanted the default behavior to show all [System.Drawing.Color] values. But I also wanted to be able to specify individual colors by name, including a wildcard, such as "azure","dark*","violet". Then I realized, why limit myself to simple wildcards? Why not support a full regex pattern like "(green)|(blue)."
The functions Name parameter is defined to accept an array of strings that can include the wildcard character. I also decided to add a qualifier parameter called "Regex," which would indicate treating the Name parameter value as a regular expression pattern.
Param(
[Parameter(
Position = 0,
ValueFromPipelineByPropertyName,
HelpMessage = "Specify a System.Drawing.Color name. Wildcards are permitted."
)]
[ValidateNotNullOrEmpty()]
[string[]]$Name,
[Parameter(HelpMessage = "Treat the Name paramter value as a regex pattern")]
[switch]$Regex
)
The Name parameter can take pipeline input by property name. I thought I might want to pipe results from my Get-DrawingColor function to this one. This means my function will have a Begin, Process, and End scriptblock.
In the Begin scriptblock, I'll define a generic list that will be updated in the Process block.
$data = [System.Collections.Generic.list[string]]::New()
In the Process block, if there is a value for the Name parameter and the use specified -Regex, PowerShell will search through a list of [System.Drawing.Color] values created in the Begin block for matching values. If a name was specified without -Regex, I'll search the same list using the -Like operator. I could have combined these steps, but that would have added complexity to the code. If the user doesn't specify any name value, then I'll use the list of all colors previously generated.
The Process block is doing nothing but building a list. I'll put my code to create the textboxes in the End scriptblock and show the form.
Show-ColorPalette
Here's the final PowerShell function. I've used Write-Verbose to provide feedback on each step of the process.
Function Show-ColorPalette {
[CmdletBinding()]
Param(
[Parameter(
Position = 0,
ValueFromPipelineByPropertyName,
HelpMessage = "Specify a System.Drawing.Color name. Wildcards are permitted."
)]
[ValidateNotNullOrEmpty()]
[string[]]$Name,
[Parameter(HelpMessage = "Treat the Name paramter value as a regex pattern")]
[switch]$Regex
)
Begin {
Write-Verbose "Starting $($MyInvocation.MyCommand)"
#get all valid color names
$all = ([system.drawing.color].GetProperties().name).Where({ $_ -notmatch "^\bIs|Name|[RGBA]\b" })
if ($Regex) {
Write-Verbose "Using Regex name matching"
}
Write-Verbose "Creating Form"
$form = New-Object System.Windows.Window
$form.Title = "Color Samples"
$form.Height = 500
$form.Width = 940
$form.top = 200
$form.left = 100
$form.UseLayoutRounding = $True
$form.WindowStartupLocation = "CenterScreen"
Write-Verbose "Creating Scrollviewer"
$scroll = New-Object System.Windows.Controls.ScrollViewer
$scroll.VerticalScrollBarVisibility = "Visible"
$scroll.HorizontalScrollBarVisibility = "Auto"
Write-Verbose "Creating Grid"
$grid = New-Object system.windows.Controls.Grid
#enable grid lines for testing and troubleshooting
$grid.ShowGridLines = $False
#initialize a collection to hold color names from pipeline
#or parameter value
$data = [System.Collections.Generic.list[string]]::New()
} #Begin
Process {
Write-Verbose "Using these PSBoundparameters"
Write-Verbose ($PSBoundParameters | Out-String)
if ($PSBoundParameters.ContainsKey("Name")) {
$pattern = $PSBoundParameters["Name"]
if ($Regex) {
Write-Verbose "Using Regex name matching"
#search the list of all color names defined in the Begin
#block for regex matches
$all.Where({ $_ -match $pattern }) | ForEach-Object {
$data.Add($_)
}
}
Else {
$Name | ForEach-Object {
$find = $_
#Search the list of all color names for that are
#like each name. This supports wildcard searches
$found = $all.where({ $_ -like "$find" })
if ($found) {
#use the value
$found | Foreach-Object {$data.Add($_)}
}
else {
Write-Warning "Failed to find any colors that match $find."
}
}
}
}
else {
#-Name was not used so use all color names
Write-Verbose "Using all defined color names"
$all.ForEach({ $data.Add($_) })
}
} #process
End {
Write-Verbose "Displaying $($data.count) items"
switch ($data.count ) {
{ $_ -eq 0 } { Write-Warning "No color values found." ; Return }
{ $_ -le 2 } { $maxRows = 1 ; Break }
{ $_ -le 5 } { $maxRows = 2 ; Break }
Default { $maxRows = $data.count / 3 -as [int] }
}
Write-Verbose "Adding $maxRows rows"
for ($i = 0; $i -lt $maxRows; $i++) {
$row = New-Object System.Windows.Controls.RowDefinition
$row.Height = 30
$grid.RowDefinitions.add($row)
}
Write-Verbose "Adding columns"
for ($i = 0; $i -lt 3; $i++) {
$col = New-Object System.Windows.Controls.ColumnDefinition
$col.Width = 300
$grid.ColumnDefinitions.Add($col)
}
$r = 0 #row index
$c = 0 #column index
foreach ($item in $data) {
$sdc = [System.Drawing.Color]::FromName($item)
if ($sdc.IsKnownColor) {
$TextBox = New-Object System.Windows.Controls.TextBox
$TextBox.Width = 275
$textbox.Height = 30
$TextBox.HorizontalAlignment = "Center"
$textbox.TextAlignment = "center"
$textbox.VerticalContentAlignment = "center"
$textbox.FontSize = 16
#The drawing color may need to be converted to a brush color
#convert color to html value
$htmlcode = "#{0:X2}{1:X2}{2:X2}" -f $sdc.r, $sdc.g, $sdc.b
Write-Verbose "Adding $item [$htmlcode]"
$textbox.background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($htmlcode)
$TextBox.Text = "$item ($($sdc.R),$($sdc.G),$($sdc.B))"
$textbox.SetValue([system.windows.controls.grid]::rowproperty, $r)
$textbox.SetValue([system.windows.controls.grid]::columnproperty, $c)
#move to the next column
$c++
$grid.AddChild($TextBox)
if ($c -eq 3) {
$c = 0
$r++
}
}
else {
#this will probably never be called
Write-Warning "Can't validate $item as a [System.Drawing.Color] value."
}
} #foreach item in data
Write-Verbose "Adding child elements to the form"
$scroll.AddChild($grid)
$form.AddChild($scroll)
#set the form to size to content is less than 15 rows
if ($maxRows -le 15) {
$form.SizeToContent = "Height"
}
Write-Verbose "Displaying the form"
#the prompt will be blocked until the form is closed
[void]$form.ShowDialog()
Write-Verbose "Ending $($MyInvocation.MyCommand)"
} #end
}
I can use the function to display several colors, including using wildcards.
Show-ColorPalette -Name "*green","violet","orangered","skyblue"
With regular expressions.
Show-ColorPalette -Name "(blue)|(green)" -Regex
Or everything by default.
Summary
I don't expect you to have a compelling business need for these functions, but that's not the point. Instead, I hope you found something valuable that you can use in your PowerShell scripting.
I would like to see the inverse of this, so maybe I’ll try it.
I mean, I would like to be able to select one -background color, then all the examples would be text in the -name colors written over the -background color.
That shouldn’t be that hard to do, given the example you’ve given
You’ll learn a lot by doing it yourself and you’ll end up with a tool that meets your needs. Have fun.