PowerShell and WPF: Ink Canvas

Today’s PowerShell and WPF topic is all about the Ink Canvas, which is a WPF control that gives you the opportunity to draw on it, just like MS Paint! There really isn’t a lot to this control and we can quickly provision it in a short amount of time. Of course, adding some fun features like changing the color of the pen and using the eraser and highlighter will require a little more work in order for it to work properly.

With that out of the way, let’s get started on building the ink canvas to use! If you have Visual Studio available, then you can quickly put together a UI and then copy the XAML code and merge it into a PowerShell script. In my case, I am building the XAML manually because there really isn’t a lot of add for this example.

[xml]$xaml = @"
<Window 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Window" WindowStyle = 'ToolWindow'
ShowInTaskbar = "True" >    
<InkCanvas x:Name="InkCanvas">
</InkCanvas>
</Window>
"@

As I mentioned, this is a basic control with pretty much no options set. I’ve cast the here-string as XML and will now take this and open it up using XMLNodeReader and then connect to the Window control.

$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

I only have to connect to a single control here, which happens to be the InkCanvas itself.

#region Connect to Controls
$InkCanvas = $Window.FindName('InkCanvas')
#endregion Connect to Controls

Now if I just show the window, we can instantly start drawing in it.

[void]$Window.ShowDialog()

image

Changing the Ink Color and Size

By default, the ink color is black. This can be changed through the $inkcanvas.DefaultDrawingAttributes.Color property. This can be a wide range of colors depending on your style. In this case, I will add a line to change it to red and then we can see the results.

$inkcanvas.DefaultDrawingAttributes.Color = ‘red’

image

We can even increase or decrease the size of the ink stylus to have a wider or narrower drawing stroke size. This is handled under the $InkCanvas.DefaultDrawingAttributes.Width and $InkCanvas.DefaultDrawingAttributes.Height properties. Adjusting these values will have a nice effect on the size as shown below:

Larger

image

Smaller

image

Setting up a Highlighter

Another nice thing that you can take advantage of with the InkCanvas is that it has a highlighter that you can use…after you select it that is.

$InkCanvas.DefaultDrawingAttributes.IsHighlighter = $True
$InkCanvas.DefaultDrawingAttributes.Color = 'Yellow'
$InkCanvas.DefaultDrawingAttributes.StylusTip = 'Rectangle'

Note here that I changed the shape of the stylus from a ellipses to a rectangle to make it more ‘highlighter like’ and also set the color to yellow. The result is a more transparent stroke that shows up when being used.

image

Let’s merge both a drawing and the highlighter into one thing and see what we can do.

image

As you can see, we can both use the ink and highlighter to accomplish a mix of things.

Erasing out Work

Of course, with drawing various things here, we certainly could use a way to erase our work or the entire canvas if we need to. First we can clear the board by calling the following method under the Strokes property object;

$InkCanvas.Strokes.Clear()

No real need for a screenshot here as a cleared canvas is pretty easy to envision.

The eraser part is the next thing to look at. We can set this under the $InkCanvas.EditingMode property by specifying one of the two options for the eraser:

  • EraseByPoint
    • Standard eraser where you drag the mouse around to erase parts of the stroke. Cursor is in a square shape.

EraseByPoint

  • EraseByStroke
    • This will actually erase the entire stroke vs. just a part of it. Cursor looks like an eraser.

EraseByStroke

With a couple of ways to erase our work, it shouldn’t be hard to clean up some accidental mistakes!

Selecting our Drawings

We can also select our drawings as well and copy, cut, resize and move them around! We first have to tell the InkCanvas that we want to select something by using the Select() method on the InkCanvas. We also need to specify an object of System.Windows.Ink.StrokeCollection.

$InkCanvas.Select((new-object System.Windows.Ink.StrokeCollection))

You will see the cursor change into a large plus (+) sign and we are now free to drag an outline of the drawing to then take action against using one of the following methods;

$InkCanvas.CopySelection()
$InkCanvas.CutSelection()
$InkCanvas.Paste()

As you can see, we are able to copy, cut or paste our ink image on the canvas. Moving and resizing occurs once we have selected an item.

SelectMethod

Saving and Loading Ink Drawings

The last thing to really cover here is saving and loading our ink drawings.

Saving involves setting up a FileStream that we can then apply to the Save() method of the $InkCanvas.Strokes property collection. We can make this process easier by creating a SaveDialog and giving it a default extension of .isf for our ink drawings.

$SaveDialog = New-Object  Microsoft.Win32.SaveFileDialog
$SaveDialog.Filter = "isf files (*.isf)|*.isf"
$Result = $SaveDialog.ShowDialog()
If ($Result){
    $FileStream = New-Object System.IO.FileStream -ArgumentList $SaveDialog.FileName, 'Create'
    $InkCanvas.Strokes.Save($FileStream)
    $FileStream.Dispose()
}

A similar approach can be done when we want to re-open an existing ink drawing by using the OpenDialog instead.

$OpenDialog = New-Object Microsoft.Win32.OpenFileDialog
$OpenDialog.Filter = "isf files (*.isf)|*.isf"
$Result = $OpenDialog.ShowDialog()
If ($Result) {
    $FileStream = New-Object System.IO.FileStream -ArgumentList $OpenDialog.FileName, 'Open'
    $StrokeCollection = New-Object System.Windows.Ink.StrokeCollection -ArgumentList $FileStream
    $InkCanvas.Strokes.Add($StrokeCollection)
    $FileStream.Dispose()
}

Our end result is something that looks like the following image:

Save_Open

I have the source code below so you can take this and have some fun with it. One last thing is that I added some keyboard shortcuts to make things such as changing the ink color, eraser, etc… more easy to do. Look for the event for Add_KeyDown.

Source Code

$Window.Add_KeyDown({ 
    $Key = $_.Key  
    If ([System.Windows.Input.Keyboard]::IsKeyDown("RightCtrl") -OR [System.Windows.Input.Keyboard]::IsKeyDown("LeftCtrl")) {
        Switch ($Key) {
            "C" {$InkCanvas.CopySelection()}
            "X" {$InkCanvas.CutSelection()}
            "P" {$InkCanvas.Paste()}
            "S" {
                $SaveDialog = New-Object  Microsoft.Win32.SaveFileDialog
                $SaveDialog.Filter = "isf files (*.isf)|*.isf"
                $Result = $SaveDialog.ShowDialog()
                If ($Result){
                    $FileStream = New-Object System.IO.FileStream -ArgumentList $SaveDialog.FileName, 'Create'
                    $InkCanvas.Strokes.Save($FileStream)
                    $FileStream.Dispose()
                }                
            }
            "O" {
                $OpenDialog = New-Object Microsoft.Win32.OpenFileDialog
                $OpenDialog.Filter = "isf files (*.isf)|*.isf"
                $Result = $OpenDialog.ShowDialog()
                If ($Result) {
                    $FileStream = New-Object System.IO.FileStream -ArgumentList $OpenDialog.FileName, 'Open'
                    $StrokeCollection = New-Object System.Windows.Ink.StrokeCollection -ArgumentList $FileStream
                    $InkCanvas.Strokes.Add($StrokeCollection)
                    $FileStream.Dispose()
                }                
            }
        }
    } Else {
        Switch ($Key) {
            "C" {
                $InkCanvas.Strokes.Clear()
            }
            "S" {
                $InkCanvas.Select((new-object System.Windows.Ink.StrokeCollection))
                #$InkCanvas.Select($InkCanvas.Strokes)
            }
            "N" {
                $Color = $Script:ColorQueue.Dequeue()
                $Script:ColorQueue.Enqueue($Color)
                $InkCanvas.DefaultDrawingAttributes.Color = $Color
            }
            "Q" {
                $This.Close()
            }
            "E" {
                Switch ($InkCanvas.EditingMode) {
                    "EraseByStroke" {
                        $InkCanvas.EditingMode = 'EraseByPoint'
                    } 
                    "EraseByPoint" {
                        $InkCanvas.EditingMode = 'EraseByStroke'
                    }
                    "Ink" {
                        $InkCanvas.EditingMode = 'EraseByPoint'
                    }
                }            
            }
            "D" {
                If ($InkCanvas.DefaultDrawingAttributes.IsHighlighter) {
                    $InkCanvas.DefaultDrawingAttributes.StylusTip = 'Ellipse'
                    $InkCanvas.DefaultDrawingAttributes.Color = 'Black'
                    $InkCanvas.DefaultDrawingAttributes.IsHighlighter = $False
                    $InkCanvas.DefaultDrawingAttributes.Height = $Script:OriginalInkSize.Height
                    $InkCanvas.DefaultDrawingAttributes.Width = $Script:OriginalInkSize.Width
                    $Script:OriginalHighLightSize.Width = $InkCanvas.DefaultDrawingAttributes.Width
                    $Script:OriginalHighLightSize.Height = $InkCanvas.DefaultDrawingAttributes.Height
                }
                $InkCanvas.EditingMode = 'Ink'            
            }
            "H" {
                If (-NOT $InkCanvas.DefaultDrawingAttributes.IsHighlighter) {
                    $Script:OriginalInkSize.Width = $InkCanvas.DefaultDrawingAttributes.Width
                    $Script:OriginalInkSize.Height = $InkCanvas.DefaultDrawingAttributes.Height
                }
                $InkCanvas.EditingMode = 'Ink'
                $InkCanvas.DefaultDrawingAttributes.IsHighlighter = $True
                $InkCanvas.DefaultDrawingAttributes.Color = 'Yellow'
                $InkCanvas.DefaultDrawingAttributes.StylusTip = 'Rectangle'
                $InkCanvas.DefaultDrawingAttributes.Width = $Script:OriginalHighLightSize.Width            
                $InkCanvas.DefaultDrawingAttributes.Height = $Script:OriginalHighLightSize.Height 
            }
            "OemPlus" {
                Switch ($InkCanvas.EditingMode) {
                    "EraseByPoint" {
                        If ($InkCanvas.EraserShape.Width -lt 20) {
                            $NewSize = $InkCanvas.EraserShape.Width + 2
                            $Rectangle = New-Object System.Windows.Ink.RectangleStylusShape -ArgumentList $NewSize,$NewSize
                            $InkCanvas.EraserShape = $Rectangle
                            $InkCanvas.EditingMode = 'None'
                            $InkCanvas.EditingMode = 'EraseByPoint'
                        }                
                    }
                    "Ink" {
                        If ($InkCanvas.DefaultDrawingAttributes.Height -lt 20) {
                            $InkCanvas.DefaultDrawingAttributes.Height = $InkCanvas.DefaultDrawingAttributes.Height + 2
                            $InkCanvas.DefaultDrawingAttributes.Width = $InkCanvas.DefaultDrawingAttributes.Width + 2
                            $InkCanvas.EditingMode = 'None'
                            $InkCanvas.EditingMode = 'Ink'
                        }
                    }
                }
                    
            }
            "OemMinus" {
                Switch ($InkCanvas.EditingMode) {
                    "EraseByPoint" {
                        If ($InkCanvas.EraserShape.Width -gt 2) {
                            $NewSize = $InkCanvas.EraserShape.Width - 2
                            $Rectangle = New-Object System.Windows.Ink.RectangleStylusShape -ArgumentList $NewSize,$NewSize
                            $InkCanvas.EraserShape = $Rectangle
                            $InkCanvas.EditingMode = 'None'
                            $InkCanvas.EditingMode = 'EraseByPoint'
                        }
                    }
                    "Ink" {
                        If ($InkCanvas.DefaultDrawingAttributes.Height -gt 2) {
                            $InkCanvas.DefaultDrawingAttributes.Height = $InkCanvas.DefaultDrawingAttributes.Height - 2
                            $InkCanvas.DefaultDrawingAttributes.Width = $InkCanvas.DefaultDrawingAttributes.Width - 2
                            $InkCanvas.EditingMode = 'None'
                            $InkCanvas.EditingMode = 'Ink'
                        }
                    }
                }            
            }
        }
    }
        
})
Posted in powershell | Tagged , , , | Leave a comment

The 2015 Scripting Games and the First Puzzle

In case you haven’t heard, the 2015 Scripting Games have taken a different approach than what has been done in the previous years. Instead of a throwing down all of the events within a 2-4 weeks span as has been done in the past, this year is more focused on an event per month style to allow folks more of an opportunity to complete the events. Also changed is the use of judges and coaches (used with the team approach last year) to review the code submitted.

Having competed, judged, coached, etc… on the Scripting Games since 2010, I am interested to see how this plays out. I will be the first to admit that judging the submissions during the games is practically a full time job to ensure that you review as many as possible by testing to see if they ran right, then running through the code to see how it was written to assign a proper score. This made for some long nights as well as auto-pilot syndrome sometimes when we were coming down to the deadline of trying to get everything judged by at least 2 judges to ensure a fair look at entries.

I could more into this but you can find all of this information out on PowerShell.org: http://powershell.org/wp/2015/06/29/the-scripting-games-heres-whats-happening/

Needless to say, this should be a fun approach and also provides a great way to get the User Groups involved as well for an added piece of fun. As the blog post says, the idea of a more familiar event could still happen in the future but the current timeline is still unsure at this point.

All that being said, the first puzzle has been published out on PowerShell.org: http://powershell.org/wp/2015/07/04/2015-july-scripting-games-puzzle/

My part in this event is that I am going to offer a celebrity submission, meaning that I will offer my solution as well as how I came to the finished piece. That won’t be published until sometime next month, but I will be sure to let everyone know when it goes live.

 

Good Luck!

Posted in powershell | Tagged , , | Leave a comment

Looking at Local Groups Using PowerShell on MCPMag

My latest series on MCPMag talks about local groups and using PowerShell (of course Winking smile) to not only report on the groups, but also to manage them. Check them out and let me know what you think!

Managing Local Groups in PowerShell

Reporting on Local Groups in PowerShell

Posted in powershell | Tagged , , , | Leave a comment

Supporting Synchronized Collections in PoshRSJob

I had a question where someone asked if PoshRSJob had support for synchronized variables (or collections) much like I have shown in the past using runspaces. This happened to be something that I touted as an advantage over the use of PSJobs and it would only make sense that you can do this using PoshRSJob as its backend framework is based on the use of runspaces and runspacepools.

Utilizing a synchronized collection for PoshRSJob really isn’t any different than what I have shown in the past. The first thing that we have to do is set up the synchronized collection that will be used to hold all of our variables and values.

$SyncHashTable = [hashtable]::Synchronized(@{})

Now I am going to add a single item to it just for reference.

$SyncHashTable.Initial = $True

We can build a small scriptblock that will be used to add some stuff to this hashtable so it is available in the other runspaces as well. We also need to ensure that the hashtable is locked at the syncroot property. Note that we are using the Enter and Exit methods in System.Threading.Monitor to accomplish this action and specifying the SyncRoot property of the hash table, not the hash table itself.

$ScriptBlock = {
    Param($SyncHash)

    [System.Threading.Monitor]::Enter($SyncHash.SyncRoot)
    $SyncHash["Job$($_)"] = [appdomain]::GetCurrentThreadId()
    Start-Sleep -Seconds (Get-Random (1..5))
    [System.Threading.Monitor]::Exit($SyncHash.SyncRoot)
}

In this case, I am going to list my job name as well as the thread id that is has. Since we are using runspacepools for PoshRSJob, it is set to reuse the runspace meaning that we should see the same thread ids and should have them even when I group the objects by value.

Next up is actually running the RSjobs and wait for everything to finish up.

1..10|Start-RSjob -Name {$_} -ScriptBlock $ScriptBlock -ArgumentList $SyncHashTable | 
Wait-RSJob –ShowProgress
LockObject_PoshRSJob

First time using GifCam, its a work in progress ;)

Once that has completed, we can see if I was right about the thread ids.

$SyncHashTable.GetEnumerator()|Group-Object -Property Value

image

As predicted, since my default throttle in Start-RSJob is set to 5 and I had 10 RSJobs queued up, we have exactly 5 thread ids that are reused. And at the end of all of this is that my synchronized  hash table was locked prior to being updated using to ensure there is no race condition that would prevent this from not working.

With that we now know how to share synchronized collections across multiple RSJobs using the PoshRSJob module. Just make sure that you utilize the Enter and Exit methods mentioned earlier to avoid possible errors and possible data loss.

Posted in powershell | Tagged , , , | Leave a comment

Managing Privileges using PoshPrivilege

A recent project of mine has been to write a module to manage privileges on a local system. What I came up is a module called PoshPrivilege that allows you to not only look at what user rights are available on a local or remote system, but also provide the ability to Add, Remove, Enable and Disable the privileges as well.

If you are running PowerShell V5, you can download this module from the PowerShell Gallery:

Install-Module –Name PoshPrivilege

Otherwise, check out my GitHub page where I am maintaining this project:

https://github.com/proxb/PoshPrivilege

I won’t spend time talking about how I wrote this module and my reasons behind it. What I will say is that instead of writing out C# code and then using Add-Type to compile it, I went with the Reflection approach of building out everything from the pinvoke signatures for methods to the Structs and even the Enums.

Let’s get started by looking at what is available in this module. The first function that is available is Get-Privilege and it comes with a few parameters. This function’s purpose is to let you view what privileges are currently available on the system (local or remote) as well as what is currently applied to your current process token.

image

A quick run through of using this function with various parameters:

Get-Privilege

image

Get-Privilege –Privilege SeAuditPrivilege, SeServiceLogonRight | Format-List

image

Get-Privilege –CurrentUser

image

If this one looks familiar, then it is probably likely that you have used the following command:

whoami /priv /fo csv | ConvertFrom-CSV

image

I opted for boolean values instead to determine the state for easier filtering if needed.

Up next are the Enable/Disable-Privilege functions. These work to Enable or Disable the privileges that are currently available on your local system to your process token. This means that if something like SeDebugPrivilege isn’t available on your system (such as being removed via Group Policy), then you cannot use Enable-Privilege to add your process token to this privilege. As in the previous image where we can see what is enabled and disabled, these are the only privileges that are available for me to work with.

To show this point, I am going to enable both SeSecurityPrivilege and SeDebugPrivilege so you can see that while the first privilege will show as Enabled, the other will not appear as it has not been made available.

Enable-Privilege -Privilege SeSecurityPrivilege,SeDebugPrivilege

SNAGHTMLd2422

As you can see from the picture, SeSecurityPrivilege has been enabled as expected, but SeDebugPrivilege is nowhere to be found. If we want SeDebugPrivilege, we will need to go about this another way which will be shown shortly.

Disabling a privilege can be done using Disable-Privilege as shown in the example below.

Disable-Privilege –Privilege SeSecurityPrivilege

SNAGHTMLfdf1c

Now that I have covered Enabling and Disabling of the privileges and their limitations, I will move onto the Add/Remove-Privilege functions which allow you to add a privilege for a user or group or remove them on a local system. Note that this only works up until it gets reverted if set by group policy. This will also note show up if you look at the privileges available on your current process token (you will log off and log back in to see it).

Remember that I do not have SeDebugPrivilege available to use? Well, now we can add it to my own account using Add-Privilege.

Add-Privilege –Privilege SeDebugPrivilege –Accountname boe-pc\proxb

image

We can see it is now available, but as I mentioned before, it doesn’t show up in my current process. A logoff and login now shows that it is not only available, but already enabled.

image

With this now enabled, we could disable it as well if needed using Disable-Privilege. I added my account for show, but we can also add groups this was as well.

As with Adding a privilege, we can remove privileges as well using Remove-Privilege.

Remove-Privilege –Privilege SeDebugPrivilege –AccountName boe-pc\proxb

image

As with Add-Privilege, you will need to log off and log back in to see the change take effect on your account.

Again, you can install this module using Install-Module if running PowerShell V5 and this project is out on GitHub to download (and contribute to as well). Enjoy!

Posted in powershell | Tagged , , , , , | 1 Comment