Create/Delete Custom View Layouts¶
Overview¶
XProtect Smart Client comes with a number of view layouts built-in. However, sometimes there is a need (or desire) to create a custom layout. The MIP SDK has supported this functionalality for quite a while, but one needed to be a developer in order to create them.
MilestonePSTools has also been able to do this for a while, since it sits on top of the MIP SDK. However, it has not been very intuitive because the view layout has to be provided in XML. An XML element is needed for each pane in the view layout and the element specifies each panes starting coordinates (in a 1000x1000 grid), as well as the width and height of the pane. Here is an example of the XML for one pane:
<ViewItems>
<ViewItem>
<Position>
<X>0</X>
<Y>0</Y>
</Position>
<Size>
<Width>500</Width>
<Height>500</Height>
</Size>
</ViewItem>
</ViewItems>
It is easy to see that this can become very cumbersome, very quickly. To assist with this, two sample scripts have been
created. One will create custom view layouts (Add-VmsViewLayout
) and the other will remove custom view layouts
(Remove-VmsViewLayout
).
Instructions¶
To use Add-VmsViewLayout
, the first step is to create a CSV file. It is strongly recommended to use an application
such as Excel, Google Sheets, LibreOffice Calc, or any other spreadsheet application that allows saving in CSV. Before
starting, it is also recommended to make the column widths roughly equal to the row heights. This isn't required, but
it helps with visualizing the layout. The number of rows and columns can be any number, but they should equal each
other. Also, note that the number of rows/columns has no bearing on how the view will look in the Smart Client. If you
can design the view layout you want with 10 rows/columns, then use that. If you need to use 20 rows/columns to get the
granularity you need, then use 20.
To create a layout, you need to put the same number in any cells that will be part of the same pane. The numbers in panes need to form squares/rectangles. If they don't, the layout will end up having unused space. Once finished, save the file as a CSV. Here is an example of a view layout that will have eight view panes.
The contents of the CSV file look like this:
1,1,1,2,2,2,2,3,3,3
1,1,1,2,2,2,2,3,3,3
1,1,1,2,2,2,2,3,3,3
4,4,4,2,2,2,2,5,5,5
4,4,4,2,2,2,2,5,5,5
4,4,4,7,7,7,7,5,5,5
4,4,4,7,7,7,7,5,5,5
6,6,6,7,7,7,7,8,8,8
6,6,6,7,7,7,7,8,8,8
6,6,6,7,7,7,7,8,8,8
In addition to specifying the view layout, a view icon can also be specified. The icon should have an equal number of pixels for its length and width, because it will be reduced down to 16x16. If no image is specified for the icon, a basic gray square will be used.
Here is a command that will create a view layout using the CSV file shown above, with a blue diamond for the icon. The layout will be placed in the '16:9' view layout folder in the Smart Client.
Add-VmsViewLayout -ViewLayoutName 'Example_Layout' -CsvPath 'C:\tmp\example_view_layout.csv' -LayoutFolder 16:9 -IconPath 'C:\tmp\blue_diamond.png'`
After that command is finished, there will be a new view layout available in the '16:9' layout folder.
Below is how the view looks like in the XProtect Smart Client.
There is also a function for removing custom view layouts. That is done by running Remove-VmsViewLayout
and specifying
the LayoutFolder
and the ViewLayoutName
. To see a list of custom layouts that have been created, you can run:
In the system where only the layout example above has been added, the output looks like this:
To remove the above custom view layout, run the following command:
Note
Even though the view layout is removed, any views that have been created using the layout will remain.
Add-VmsViewLayout¶
function Add-VmsViewLayout {
<#
.SYNOPSIS
Adds a new view layout to a Milestone XProtect system.
.DESCRIPTION
Takes a CSV file that contains numbers representing panes and converts it into a new view layout. An image can also
be specified for the icon of the view layout. Remove-VmsViewLayout (separate function) can be used to remove any
custom view layouts that have been created with Add-VmsViewLayout.
Use Excel, Google Sheets, LibreOffice Calc, or any other spreadsheet program that can save in CSV format to make it
easier to create the necessary file. It is recommended to make the row heights and column widths in the spreadsheet
application roughly the same size to get a good visual. You can use as many rows/columns as seems beneficial for
your purposes of designing the layout. The script will convert everything to work properly in the Smart Client.
Note that if the number of rows and columns is not the same, the view will be stretched to make them the same so it is
recommended to use equal number of rows and columns. Also note that the number of rows/columns has no bearing on how
the view will look in the Smart Client. If you can design the view layout you want with 10 rows/columns, then use
that. If you need to use 20 rows/columns to get the granularity you need, then use 20.
To create a layout, you need to put the same number in any cells that will be part of the same pane. The numbers in
panes need to form squares/rectangles. If they don't, the layout will end up being wonky. Once finished, save the file
as a CSV. Here is an ASCII "visual" of an example of what the spreadsheet might look like (the numbers in the first
column are designating the row numbers, for visual purposes).
| A | B | C | D | E | F | G | H | I | J |
---+---+---+---+---+---+---+---+---+---+---+
1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
---+---+---+---+---+---+---+---+---+---+---+
2 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
---+---+---+---+---+---+---+---+---+---+---+
3 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
---+---+---+---+---+---+---+---+---+---+---+
4 | 3 | 3 | 3 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
---+---+---+---+---+---+---+---+---+---+---+
5 | 3 | 3 | 3 | 4 | 4 | 4 | 5 | 5 | 5 | 5 |
---+---+---+---+---+---+---+---+---+---+---+
6 | 3 | 3 | 3 | 4 | 4 | 4 | 5 | 5 | 5 | 5 |
---+---+---+---+---+---+---+---+---+---+---+
7 | 3 | 3 | 3 | 4 | 4 | 4 | 5 | 5 | 5 | 5 |
---+---+---+---+---+---+---+---+---+---+---+
8 | 3 | 3 | 3 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
---+---+---+---+---+---+---+---+---+---+---+
9 | 3 | 3 | 3 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
---+---+---+---+---+---+---+---+---+---+---+
10 | 3 | 3 | 3 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
---+---+---+---+---+---+---+---+---+---+---+
The above "spreadsheet" would have CSV contents that looked like this:
1,1,1,2,2,2,2,2,2,2
1,1,1,2,2,2,2,2,2,2
1,1,1,2,2,2,2,2,2,2
3,3,3,2,2,2,2,2,2,2
3,3,3,4,4,4,5,5,5,5
3,3,3,4,4,4,5,5,5,5
3,3,3,4,4,4,5,5,5,5
3,3,3,6,6,6,6,6,6,6
3,3,3,6,6,6,6,6,6,6
3,3,3,6,6,6,6,6,6,6
The above CSV contents will create a view layout that looks like this. Depending on what editor or PowerShell console you
view the below in, it may be stretched vertically. That can be ignored. All view layouts in the Smart Client have a grid
layout of 1000x1000.
+---+---+---+---+---+---+---+---+---+---+
| | |
+ + +
| 1 | |
+ + 2 +
| | |
+---+---+---+ +
| | |
+ +---+---+---+---+---+---+---+
| | | |
+ + + +
| | 4 | 5 |
+ + + +
| 3 | | |
+ +---+---+---+---+---+---+---+
| | |
+ + +
| | 6 |
+ + +
| | |
+---+---+---+---+---+---+---+---+---+---+
.PARAMETER ViewLayoutName
Specify the name the new view layout should have.
.PARAMETER CsvPath
Specify the path of the CSV file.
.PARAMETER LayoutFolder
Specify which view layout group the new view should be added to
.PARAMETER IconPath
Specify the path to an image that will be used for the icon. Supported formats are JPG, JPEG, GIF, BMP, PNG, TIF, TIFF,
WMP, and ICO. The image will be resized to 16x16 pixels. If no image is specified, the icon will be a basic gray square.
.PARAMETER Description
Specify a description of the new view layout. This is optional.
.EXAMPLE
# Connect-Vms only required if not already connected
Connect-Vms -ShowDialog -AcceptEula
Add-VmsViewLayout -ViewLayoutName 'Sample View' -CsvPath 'C:\tmp\layout.csv' -LayoutFolder '16:9' -IconPath 'C:\tmp\view_icon.png'
Adds a new view layout to the 16:9 folder
.NOTES
The software provided by Milestone Systems, Inc. (hereinafter referred to as "the Software") is provided on
an "as is" basis, without any warranties or representations, express or implied, including but not limited to
the implied warranties of merchantability, fitness for a particular purpose, or non-infringement.
Warranty Disclaimer:
The Software is provided without any warranty of any kind, whether expressed or implied. Milestone Systems, Inc.
expressly disclaims all warranties, conditions, and representations, including but not limited to warranties of
title, non-infringement, merchantability, or fitness for a particular purpose. The entire risk arising out of the
use or performance of the Software remains with the user.
Support Disclaimer:
Milestone Systems, Inc. does not provide any support or maintenance services for the Software. The user acknowledges
and agrees that Milestone Systems, Inc. shall have no obligation to provide any updates, bug fixes, or technical
support for the Software, whether through telephone, email, or any other means.
User Responsibility:
The user acknowledges and agrees that they are solely responsible for the selection, installation, use, and results
obtained from the Software. Milestone Systems, Inc. shall not be held liable for any errors, defects, or damages arising
from the use or inability to use the Software, including but not limited to direct, indirect, incidental, consequential,
or special damages.
Indemnification:
The user agrees to indemnify, defend, and hold harmless Milestone Systems, Inc. and its directors, officers, employees,
and agents from any and all claims, liabilities, damages, losses, costs, and expenses (including reasonable attorneys' fees)
arising out of or related to the user's use or misuse of the Software.
By using the Software, the user acknowledges that they have read and understood this clause and agree to be bound by its terms.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]
$ViewLayoutName,
[Parameter(Mandatory)]
[ValidatePattern('\.(csv)$')]
[string]
$CsvPath,
[Parameter(Mandatory)]
[ValidateSet('4:3','16:9','4:3 Portrait','16:9 Portrait')]
[string]
$LayoutFolder,
[Parameter()]
[ValidatePattern('\.(jpg|jpeg|gif|bmp|png|tif|tiff|wmp|ico)$')]
[string]
$IconPath,
[Parameter(Mandatory=$false)]
[string]
$Description
)
$ms = Get-VmsManagementServer -ErrorAction SilentlyContinue
if ([string]::IsNullOrEmpty($ms.Version)) {
Write-Warning "Please connect to a Milestone XProtect system first."
break
}
$layoutGroup = $ms.LayoutGroupFolder.LayoutGroups | Where-Object {$_.Name -eq $LayoutFolder}
if (-not [string]::IsNullOrEmpty($IconPath)) {
# Resize image so either the width or height (or both) is 16 pixels
$oldImage = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $IconPath
$oldHeight = $oldImage.Height
$oldWidth = $oldImage.Width
if ($oldHeight -gt $oldWidth) {
$ratio = $oldHeight / 16
[int]$height = $oldHeight / $ratio
[int]$width = $oldWidth / $ratio
}
elseif ($oldWidth -gt $oldHeight) {
$ratio = $oldWidth / 16
[int]$width = $oldWidth / $ratio
[int]$height = $oldHeight / $ratio
}
else {
$ratio = $oldHeight / 16
[int]$height = $oldHeight / $ratio
[int]$width = $oldWidth / $ratio
}
$bitmap = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $width, $height
$newImage = [System.Drawing.Graphics]::FromImage($bitmap)
$newImage.DrawImage($oldImage, $(New-Object -TypeName System.Drawing.Rectangle -ArgumentList 0, 0, $width, $height))
$dot = $iconPath.LastIndexOf(".")
$extension = $iconPath.Substring($dot + 1,$iconPath.Length - $dot - 1)
switch ($extension) {
'jpg' {$extendedExtension = 'jpeg';break}
'tif' {$extendedExtension = 'tiff';break}
'ico' {$extendedExtension = 'icon';break}
default {$extendedExtension = $extension}
}
$outputPath = "$($env:TEMP)\tmpImage.$($extension)"
$bitmap.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::$extendedExtension)
[string]$iconBase64 = [convert]::ToBase64String((Get-Content "$($env:TEMP)\tmpImage.$($extension)" -Raw -Encoding Byte))
Remove-Item -Path "$($env:TEMP)\tmpImage.$($extension)" -Force
} else {
# If no image is provided for the icon, it is set to a plain, gray rectangle similar to the standard view icons but
# just blank inside.
$iconBase64 = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAThJREFUOE+tk7FqwzAURe+TMHRz+w/t0vYP+iWmmz0YPBhCPOUfsiQEMhgPxlvWflAS2g4ldGkSRSbNK1KjEkwGx/QtQqB7dd59EgEgdCs2sq7ivyvJ9/2bSwE8z9uvVqs1AKZ+v/95gcFBCCG01svRaPQEYEODwcD20qaYGVJKKKU+hsPhLYAvyrLs0EZ8PGMJlFKv4/H4HsCaer1eawIA1mC32y0nk4kx2FCapobATMMYuak40+aejwbz6XT6AGBLSZJ0IVjkeW4IthTH8TmDJoGLyU1hXhTFL0EURS5EJzptpZnvgYhEXdeLsiwfrUEYhmzG07aEENBav1dVdWcNgiB4YeZrIroCIO37JjJU32bPzMKFzMxaSkl1Xb/NZrNnAMqkbERmbfMvTlH3//OZWt58LiJL8wMo2oQR1nRVYQAAAABJRU5ErkJggg=='
}
# Loop through each row in the CSV
$csv = Get-Content -Path $CsvPath
$dataArray = New-Object System.Collections.ArrayList
for ($i = 0;$i -lt $csv.Length;$i++) {
$null = $dataArray.Add($csv[$i].Split(','))
}
# Get unique numbers in CSV so we know how many view panes there are
$flattend = $dataArray | ForEach-Object {$_}
$unique = $flattend | Select-Object -Unique
$xml = New-Object System.Xml.XmlDocument
$viewLayout = $xml.CreateElement("ViewLayout")
$viewLayout.SetAttribute("ViewLayoutType","VideoOS.RemoteClient.Application.Data.ViewLayouts.ViewLayoutCustom, VideoOS.RemoteClient.Application")
$viewLayout.SetAttribute("ViewLayoutGroupId",$layoutGroup.Id)
$null = $xml.AppendChild($viewLayout)
$viewLayoutIcon = $xml.CreateElement("ViewLayoutIcon")
$viewLayoutIcon.InnerText = $iconBase64
$null = $viewLayout.AppendChild($viewLayoutIcon)
$rows = $csv.Length
$columns = [math]::Round($csv[0].Length / 2)
$unique | ForEach-Object {
$valueToFind = $_
$locations = @()
for ($i = 0; $i -lt $dataArray.Count; $i++) {
for ($j = 0; $j -lt $dataArray[0].Count; $j++) {
if ($dataArray[$i][$j] -eq $valueToFind) {
$locations += "$j,$i" # Need to list it as "$j,$i" because $j is x-coord and $i is y-coord
}
}
}
$xCoord = [math]::Round([int]($locations[0].Split(",")[0]) * (1000 / $columns)) # Milestone views have 1000 points on X axis
$yCoord = [math]::Round([int]($locations[0].Split(",")[1]) * (1000 / $rows)) # Milestone views have 1000 points on Y axis
$firstX = [int]($locations[0].Split(",")[0])
$firstY = [int]($locations[0].Split(",")[1])
$lastX = [int]($locations[-1].Split(",")[0])
$lastY = [int]($locations[-1].Split(",")[1])
$widthValue = [math]::Round(($lastX - $firstX + 1) * (1000 / $columns)) # Milestone views have 1000 points on X axis
$heightValue = [math]::Round(($lastY - $firstY + 1) * (1000 / $rows)) # Milestone views have 1000 points on X axis
$viewItems = $xml.CreateElement("ViewItems")
$viewItem = $xml.CreateElement("ViewItem")
$position = $xml.CreateElement("Position")
$size = $xml.CreateElement("Size")
$x = $xml.CreateElement("X")
$x.InnerText = $xCoord
$null = $position.AppendChild($x)
$y = $xml.CreateElement("Y")
$y.InnerText = $yCoord
$null = $position.AppendChild($y)
$posWidth = $xml.CreateElement("Width")
$posWidth.InnerText = $widthValue
$null = $size.AppendChild($posWidth)
$posHeight = $xml.CreateElement("Height")
$posHeight.InnerText = $heightValue
$null = $size.AppendChild($posHeight)
$null = $viewItem.AppendChild($position)
$null = $viewItem.AppendChild($size)
$null = $viewItems.AppendChild($viewItem)
$null = $viewLayout.AppendChild($viewItems)
}
$null = $xml.AppendChild($viewLayout)
$null = $layoutGroup.LayoutFolder.AddLayout($ViewLayoutName,$Description,$xml.OuterXml)
}
Remove-VmsViewLayout¶
function Remove-VmsViewLayout {
<#
.SYNOPSIS
Removes a view layout, that has previously been added, from the Milestone XProtect system
.DESCRIPTION
Removes a custom view layout that was added via Add-VmsViewLayout or some other method. The view layout name and the
layout group need to be provided.
.PARAMETER ViewLayoutName
Specify the name of the view to be removed.
.PARAMETER LayoutFolder
Specify which view layout group the view to be removed resides in.
.PARAMETER ListCustomLayouts
List all custom layouts along with the Layout Folder they belong to.
.EXAMPLE
Remove-VmsViewLayout -ViewLayoutName 'Sample View' -LayoutFolder '16:9'
Removes custom view layout named 'Sample View'
.EXAMPLE
# Connect-Vms only required if not already connected
Connect-Vms -ShowDialog -AcceptEula
Remove-VmsViewLayout -ListCustomLayouts
Returns a list of all custom layouts and which Layout Folder they belong to
.NOTES
The software provided by Milestone Systems, Inc. (hereinafter referred to as "the Software") is provided on
an "as is" basis, without any warranties or representations, express or implied, including but not limited to
the implied warranties of merchantability, fitness for a particular purpose, or non-infringement.
Warranty Disclaimer:
The Software is provided without any warranty of any kind, whether expressed or implied. Milestone Systems, Inc.
expressly disclaims all warranties, conditions, and representations, including but not limited to warranties of
title, non-infringement, merchantability, or fitness for a particular purpose. The entire risk arising out of the
use or performance of the Software remains with the user.
Support Disclaimer:
Milestone Systems, Inc. does not provide any support or maintenance services for the Software. The user acknowledges
and agrees that Milestone Systems, Inc. shall have no obligation to provide any updates, bug fixes, or technical
support for the Software, whether through telephone, email, or any other means.
User Responsibility:
The user acknowledges and agrees that they are solely responsible for the selection, installation, use, and results
obtained from the Software. Milestone Systems, Inc. shall not be held liable for any errors, defects, or damages arising
from the use or inability to use the Software, including but not limited to direct, indirect, incidental, consequential,
or special damages.
Indemnification:
The user agrees to indemnify, defend, and hold harmless Milestone Systems, Inc. and its directors, officers, employees,
and agents from any and all claims, liabilities, damages, losses, costs, and expenses (including reasonable attorneys' fees)
arising out of or related to the user's use or misuse of the Software.
By using the Software, the user acknowledges that they have read and understood this clause and agree to be bound by its terms.
#>
[CmdletBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Example function.')]
param (
[Parameter(Mandatory, ParameterSetName = 'Remove')]
[string]
$ViewLayoutName,
[Parameter(Mandatory, ParameterSetName = 'Remove')]
[ValidateSet('4:3','16:9','4:3 Portrait','16:9 Portrait')]
[string]
$LayoutFolder,
[Parameter(Mandatory, ParameterSetName = 'List')]
[switch]
$ListCustomLayouts
)
$ms = Get-VmsManagementServer -ErrorAction SilentlyContinue
if ([string]::IsNullOrEmpty($ms.Version)) {
Write-Warning "Please connect to a Milestone XProtect system first."
break
}
$layoutGroups = $ms.LayoutGroupFolder.LayoutGroups
$customViews = New-Object System.Collections.Generic.List[PSCustomObject]
if ($ListCustomLayouts) {
foreach ($lg in $layoutGroups) {
$task = $lg.LayoutFolder.RemoveLayout()
($task.ItemSelectionValues).Keys | ForEach-Object {
$viewName = $_
$row = [PSCustomObject]@{
"View Layout Name" = $viewName
"View Layout Folder" = $lg.Name
}
$customViews.Add($row)
}
}
if (-not [string]::IsNullOrEmpty($customViews.'View Layout Folder')) {
return $customViews
break
} else {
Write-Warning "There are no custom view layouts in this system."
break
}
}
$layoutGroup = $layoutGroups | Where-Object {$_.Name -eq $LayoutFolder}
$layout = $layoutGroup.LayoutFolder.Layouts | Where-Object {$_.Name -eq $ViewLayoutName}
if ([string]::IsNullOrEmpty($layout.Id)) {
Write-Warning 'The selected view does not exist in the selected view layout group.'
break
}
$null = $layoutGroup.LayoutFolder.RemoveLayout($layout.Path)
}