I am trying to update an old powershell script. It currently has a GUI built in the script, and the updated GUI was built with WPF, but still is a powershell script. It mostly works, but the problem I am having is this: The script's purpose is to move folders from a source to a fixed destination. The user selects the folder and then chooses which files to move, and the "load files" button should update with the count and total size of files. The old script does this correctly, however, the new script will not. With a PS built GUI, I can use "SelectedIndices"; with the WPF built GUI, I can only use "SelectedIndex", and have yet to find a workaround so the button will properly update. Currently it keeps the size of the first selected item. Does anyone know a solution to this? I have been searching and using trial-and-error all week, with no success. I am adding the relevant pieces of code and result photos below. Edited to provide full sample
Set-StrictMode -Version Latest
Add-Type -AssemblyName PresentationFramework # WPF assemblies
Add-Type -AssemblyName System.Windows.Forms # .NET assemblies
if (-not $env:dataFolder) {
$env:dataFolder = "D:"
}
$addModulePath = Join-Path -Path $env:dataFolder -ChildPath "Libraries\modules"
if (-not $env:PSModulePath.Contains($addModulePath)) {
$env:PSModulePath = $addModulePath + ";" + $env:PSModulePath
}
$Global:mainFolder = 'D:\Database\PackageFiles'
$Global:appFolder = "$env:dataFolder\Tools\databaseUpdateTool"
$Global:Icon = "$env:dataFolder\Libraries\icons\redAppLogo.ico"
# Generic Globals - - verify if there will be any changes
$Global:untouchablePackages = #("package names that cannot be deleted show here as an array")
#___ Class Definitions ___
class Package {
[String]$name = $null
[String]$fullName = $null
[int64]$size = 0
[String]$pickList = $null
#Constructors
# Return and null Structure
Package() {}
Package($folder) {
$theSize = getFolderSize $folder.fullName
$this.name = $folder.name
$this.size = $theSize
$this.fullName = $folder.fullName
$this.pickList = ($theSize / 1gb).ToString('0.00').padleft(9) + ' GB ' + $folder.name
}
# Takes a package, returns a duplicate
[Package]duplicate($newPath) {
$newPackage = [Package]::New()
$newPackage.name = $this.name
$newPackage.size = $this.size
$newPackage.pickList = $this.pickList
$newPackage.fullName = $this.fullName
if ($newPackage -ne $null) {
$newPackage.fullName = (Join-Path -Path $newPath -ChildPath $this.name)
}
return $newPackage
}
}
#___ Functions ___
function getArraySize ($anObject) {
if ($anObject) {
if ($anObject -is [System.Array]) {
$theSize = $anObject.count
} else {
$theSize = 1
}
} else {
$theSize = 0
}
return [int]$theSize
}
function getFolderSize {
[CmdletBinding()]
param (
[parameter(position=0, ValueFromPipeline=$false)] [string] $aFolder
)
$aList = Get-ChildItem -force $aFolder
$size = 0
foreach ($member in $aList) {
if ($member.gettype().name -eq 'FileInfo') {
$size = $size + $member.length
} else {
$size = $size + (getFolderSize $member.fullname)
}
}
return $size
}
# copyPackageFolder allows users to copy location packages into the D: drive
function copyPackageFolder {
[CmdletBinding()]
param (
[parameter(position=0, ValueFromPipeline=$false)] $target
)
try {
$results1 = Copy-Item -Path $target.fullName -Destination $Global:removeFolder -Recurse
$results1
$returnValue = $results1 -eq $null
} catch {
$returnValue = $false
}
return $returnValue
}
# removePackageFolder is similar to copyPackageFolder, with a Remove-Item instead of Copy-Item
function removePackageFolder {
[CmdletBinding()]
param (
[parameter(position=0, ValueFromPipeline=$false)] $removePackage
)
try {
Remove-Item -LiteralPath $removePackage.fullName -Recurse -Force
$returnValue = $true
} catch {
$returnValue = $false
}
return $returnValue
}
function getPickListFromFolder {
# Extracts names and sizes of top-level directories and returns a list
[CmdletBinding()]
param (
[parameter(position=0, ValueFromPipeline=$true)] [string] $aFolder
)
$folderList = Get-ChildItem $aFolder
$myFolderList = #()
foreach($folder in ($folderList | sort name)) {
if ($folder.gettype().name -eq 'DirectoryInfo') {
$theSize = getFolderSize $folder.fullname
$folderObject = [Package]::New($folder)
if (-not $untouchablePackages.contains($folderObject.name)) {
$myFolderList += $folderObject
}
}
}
if ($myFolderList.count -gt 0) {
return ,($myFolderList)
} else {
return $null
}
}
function getFolderName {
[CmdletBinding()]
param (
[parameter(position=0, ValueFromPipeline=$false)] [string]$windowText,
[parameter(position=1, ValueFromPipeline=$false)] [string]$defaultFolder
)
#
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
$openFolderDialog = New-Object System.Windows.Forms.folderBrowserDialog
if ($defaultFolder) {$openFolderDialog.SelectedPath = $defaultFolder}
if ($windowText) {
$openFolderDialog.Description = $windowText
} else {
$openFolderDialog.Description = 'Please select a folder'
}
$openFolderDialog.RootFolder = 'MyComputer'
$buttonAnswer = $openFolderDialog.ShowDialog()
if ($buttonAnswer -match 'ok') {
return $openFolderDialog.SelectedPath
} else {
return $null
}
}
function loadUpPackages {
# This is the function that executes when the Load button is clicked
# First it checks if a folder with the same name exists in the destination folder
# (if so, it prompts the user if they want to replace it)
# Then it executes the copyPackageFolder function
[CmdletBinding()]
param (
[parameter(position=0, ValueFromPipeline=$true)] [Package]$loadPackage
)
BEGIN { }
PROCESS {
if ($Global:removeObjList) {
$conflictingPackage = $Global:removeObjList | Where-Object { $_.name -eq $loadPackage.name }
} else {
$conflictingPackage = $null
}
if ($conflictingPackage) {
switch ([System.Windows.Forms.MessageBox]::Show("The destination already contains a folder named " + $loadPackage.name + " - would you like to replace it?","databaseUpdateTool",'YesNo','None')) {
'Yes' {
if (removePackageFolder $conflictingPackage) {
$GLOBAL:removeListBox.Items.Remove($conflictingPackage.pickList)
$Global:removeObjList = $Global:removeObjList | Where-Object { $_.name -ne $loadPackage.name }
if (copyPackageFolder $loadPackage) {
$duplicatePackage = $loadPackage.duplicate($GLOBAL:removeFolder)
switch (getArraySize $Global:removeObjList) {
0 {$Global:removeObjList = $duplicatePackage }
1 {$Global:removeObjList = ($Global:removeObjList, $duplicatePackage)}
Default {$Global:removeObjList += $duplicatePackage}
}
$Global:removeListBox.Items.add($duplicatePackage.pickList)
} else {
$message = ("Unable to load package $loadPackage.name", 'Please try again')
$title = 'Load error message'
$buttons = [system.windows.forms.messageboxbuttons]::OK
$results = [system.windows.forms.messagebox]::show($message, $title, $buttons)
}
} else {
$message = ("Unable to remove package $loadPackage.name", 'Please try again')
$title = 'Remove error message'
$buttons = [system.windows.forms.messageboxbuttons]::OK
$results = [system.windows.forms.messagebox]::show($message, $title, $buttons)
}
}
'No' {}
default {}
}
} else {
if (copyPackageFolder $loadPackage) {
$newPackage = $loadPackage.duplicate($GLOBAL:removeFolder)
switch (getArraySize $Global:removeObjList) {
0 {$Global:removeObjList = $newPackage }
1 {$Global:removeObjList = ($Global:removeObjList, $newPackage)}
Default {$Global:removeObjList += $newPackage}
}
$Global:removeListBox.items.add($newPackage.pickList)
} else {
$message = ("Unable to load package $loadPackage.name", 'Please try again')
$title = 'Load error message'
$buttons = [system.windows.forms.messageboxbuttons]::OK
$results = [system.windows.forms.messagebox]::show($message, $title, $buttons)
}
}
}
END {
$loadListBox.Clear()
if ($Global:removeObjList -eq $null) { $Global:removeObjList = #() }
}
}
function removePickList {
# Function output must be explicitly declared as an array - ",( <output here> )"
$Global:removeObjList = getPickListFromFolder $Global:removeFolder
if ($Global:removeObjList -ne $null) {
foreach ($item in $Global:removeObjList.pickList) {
$theIndex = $GLOBAL:removeListBox.Items.Add($item)
}
}
}
function removePackages {
[CmdletBinding()]
param (
[parameter(position=0, ValueFromPipeline=$true)] $removePackage
)
BEGIN {
}
PROCESS {
if (removePackageFolder $removePackage) {
$GLOBAL:removeListBox.Items.Remove($removePackage.pickList)
$GLOBAL:removeObjList = $GLOBAL:removeObjList | Where { $_ -ne $removePackage }
} else {
$packageName = $removePackage.name
$message = ("Unable to remove package $packageName - Please try again")
$title = 'Remove error message'
$buttons = [system.windows.forms.messageboxbuttons]::OK
$results = [system.windows.forms.messagebox]::show($message, $title, $buttons)
}
}
END {
if ($Global:removeObjList -eq $null) { $Global:removeObjList = #() }
}
}
#___ GUI Functions ___
function getXAMLgui {
# Read in and process the XAML GUI file.
param (
[parameter(Mandatory=$false)] [string] $xamlName = $null
)
$xamlPath = Join-Path -Path $env:dataFolder -ChildPath "Libraries"
$xamlFile = Join-Path -Path $xamlPath -ChildPath $xamlName
$xamlData = New-Object xml
$xamlData.Load($xamlFile)
$xamlNodeReader = [System.Xml.XmlNodeReader]::new($xamlData)
$form = [Windows.Markup.XamlReader]::Load($xamlNodeReader)
return $form
}
function loadBrowseButtonClick{
$loadPackagesFolder = getFolderName 'Select top level folder containing packages'
if ($loadPackagesFolder -ne $null) {
$loadPFtextBox.Text = $loadPackagesFolder
$Global:loadObjList = getPickListFromFolder $loadPackagesFolder
$loadListBox.Items.Clear()
if ($Global:loadObjList -ne $null) {
foreach ($item in $Global:loadObjList.pickList) {
$theIndex = $Global:loadListBox.Items.Add($item)
}
}
$loadButton.Content = "load 0 packages (0 GB)"
}
}
function loadButtonClick{
$loadMsgBoxTitle = 'Load Packages Message'
$loadMsgBoxMessage = "Proceed with loading $Global:loadSize GB of data?"
$loadMsgBoxButtons = [system.windows.forms.messageboxbuttons]::YESNO
$results = [system.windows.forms.messagebox]::show($loadMsgBoxMessage, $loadMsgBoxTitle, $loadMsgBoxButtons)
switch ($results) {
'Yes' {
$Global:loadObjList[$Global:loadSelection] | loadUpPackages
}
}
}
function loadSelectionChange{
$Global:loadSelection = $loadListBox.SelectedIndex
if ($Global:loadSelection -gt 0) {
switch (getArraySize $Global:loadObjList) {
0 {
$Global:loadSize = 0
$numberOfPackages = 0
$loadButton.Content = "Load $numberOfPackages packages ($Global:loadSize GB)"
$loadButton.IsEnabled = $false
break
}
1 {
$Global:loadSize = ($Global:loadObjList.size / 1gb).ToString('0.00')
$numberOfPackages = 1
$loadButton.Content = "Load $numberOfPackages packages ($Global:loadSize GB)"
if ($Global:freeSpace -lt $Global:loadSize) {
$loadButton.IsEnabled = $false
} else {
$loadButton.IsEnabled = $true
}
break
}
default {
$Global:loadSize = (($Global:loadObjList[$Global:loadSelection].size | Measure-Object -Sum).sum / 1gb).ToString('0.00')
$numberOfPackages = $loadListBox.SelectedItems.Count
$loadButton.Content = "Load $numberOfPackages packages ($Global:loadSize GB)"
if ($Global:freeSpace -lt $Global:loadSize) {
$loadButton.IsEnabled = $false
} else {
$loadButton.IsEnabled = $true
}
}
}
$Global:removeListBox.Clear()
} else {
$Global:loadSize = 0
$numberOfPackages = 0
$loadButton.Content = "Load $numberOfPackages packages ($Global:loadSize GB)"
$loadButton.IsEnabled = $false
}
}
function removeButtonClick{
$removeMsgBoxTitle = 'Remove Packages Message'
$removeMsgBoxMessage = "Proceed with removal of $Global:removeSize GB of data?"
$removeMsgBoxButtons = [system.windows.forms.messageboxbuttons]::OKCancel
$results = [system.windows.forms.messagebox]::show($removeMsgBoxMessage, $removeMsgBoxTitle, $removeMsgBoxButtons)
switch ($results) {
'OK' {
$Global:removeObjList[$Global:removeSelection] | removePackages
}
}
}
function removeSelectionChange{
$Global:removeSelection = $GLOBAL:removeListBox.SelectedItems
if ($Global:removeSelection.count -gt 0) {
switch (getArraySize $Global:removeObjList) {
0 {
$Global:removeSize = 0
$numberOfPackages = 0
$removeButton.Text = "Remove $numberOfPackages packages ($Global:removeSize GB)"
$removeButton.IsEnabled = $false
break
}
1 {
$Global:removeSize = ($Global:removeObjList.size / 1gb).ToString('0.00')
$numberOfPackages = 1
$removeButton.Text = "Remove $numberOfPackages packages ($Global:removeSize GB)"
$removeButton.IsEnabled = $true
break
}
default {
$Global:removeSize = (($Global:removeObjList[$Global:removeSelection].size | Measure-Object -Sum).sum / 1gb).ToString('0.00')
$numberOfPackages = $GLOBAL:removeListBox.SelectedItems.Count
$removeButton.Text = "Remove $numberOfPackages packages ($Global:removeSize GB)"
$removeButton.IsEnabled = $true
}
}
$loadListBox.Clear()
} else {
$Global:removeSize = 0
$numberOfPackages = 0
$removeButton.Text = "Remove $numberOfPackages packages ($Global:removeSize GB)"
$removeButton.IsEnabled = $false
}
}
#___ Main Program ___
$Global:removeFolder = $Global:mainFolder
$destinationDrive = $removeFolder.Substring(0,1)
$Global:freeSpace = (get-psdrive $destinationDrive).free
$freeSpaceGB = ($Global:freeSpace / 1gb).ToString('0')
#load main window
$mainWindow = getXamlgui -xamlName "databaseUpdateGUI.xml"
#----- Find all the goodies -----#
#Text windows
$loadPFtextBox = $mainWindow.FindName("loadSearchRtb")
$freeSpaceLabel = $mainWindow.FindName("freeSpaceLabel")
$removeListBox = $mainWindow.FindName("removePackagesBox")
$loadListBox= $mainWindow.FindName("selectPackagesBox")
$removePFtextBox = $mainWindow.FindName("removePFtextBox")
#Buttons
$searchButton = $mainWindow.FindName("browseButton")
$loadButton = $mainWindow.FindName("loadPackagesButton")
$removePackagesButton = $mainWindow.FindName("removePackagesButton")
#----- Set properties and actions -----#
$freeSpaceLabel.Content.Text = $freeSpaceGB + 'GB Free'
$loadListBox.SelectionMode = 'Extended'
$loadListBox.add_SelectionChanged($Function:loadSelectionChange)
$removeListBox.SelectionMode = 'Extended'
$removeListBox.add_SelectionChanged($Function:removeSelectionChange)
$removePFtextBox.Text = "$Global:removeFolder"
$searchButton.add_click($Function:loadBrowseButtonClick)
$loadButton.add_click($Function:loadButtonClick)
$removePackagesButton.add_click($Function:removeButtonClick)
#Load the form
$mainWindow.ShowDialog() | Out-Null
XML sheet. Note, styles taken out as code was too many characters
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Database Update Tool"
Height="600"
Width="1000"
Background="Gainsboro"
ResizeMode="NoResize">
<Grid Margin="2,2,12,2"
Background="Gainsboro">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="380"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="499"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="499"/>
</Grid.ColumnDefinitions>
<!-- Begin adding Items-->
<!-- Left Column -->
<Label Grid.Row="0"
Margin="10,0,10,0"
Content="Load Folder Contents"
HorizontalContentAlignment="Center"
FontSize="16"
FontWeight="Bold"
FontFamily="Calibri">
</Label>
<Label Grid.Row="1"
Margin="10,0,10,0"
FontSize="14"
FontWeight="DemiBold"
Content="Select the location of loadable packages:"
FontFamily="Calibri">
</Label>
<TextBlock x:Name="loadSearchRtb"
Grid.Row="2"
Background="White"
Margin="10,0,10,0"
Width="400"
HorizontalAlignment="Left"
FontSize="16">
</TextBlock>
<Button Grid.Row="2"
x:Name="browseButton"
Margin="10,0,35,0"
Content="..."
Width="40"
FontSize="14"
HorizontalAlignment="Right"
Background="Gray">
</Button>
<Label Grid.Row="3"
Margin="10,0,10,0"
FontSize="14"
FontWeight="DemiBold"
Content="Select packages to load onto the system"
FontFamily="Calibri">
</Label>
<ListBox x:Name="selectPackagesBox"
Background="White"
Grid.Row="4"
Margin="10,0,10,5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
</ListBox>
<Button Grid.Row="5"
x:Name="loadPackagesButton"
Content="Load 0 packages (0GB)"
Width="225"
Height="35"
Margin="10,10,10,10"
Background="ForestGreen"
Foreground="White"
FontFamily="Calibri"
FontSize="16">
</Button>
<!-- Grid splitter (center column) -->
<GridSplitter Grid.Column="1"
Grid.Row="0"
Grid.RowSpan="6"
Width="2"
Background="Black"
IsEnabled="False">
</GridSplitter>
<!-- Right Column -->
<Label Grid.Column="2"
Grid.Row="0"
Content="Destination Folder Contents"
HorizontalContentAlignment="Center"
FontSize="16"
FontWeight="Bold"
FontFamily="Calibri">
</Label>
<Label Grid.Column="2"
Grid.Row="1"
FontSize="14"
FontWeight="DemiBold"
Margin="10,0,10,0"
Content="Location of packages:"
FontFamily="Calibri">
</Label>
<TextBlock x:Name="removePFtextBox"
Background="WhiteSmoke"
Grid.Column="2"
Grid.Row="2"
FontFamily="Calibri"
FontSize="16"
FontWeight="DemiBold"
Margin="10,0,10,0"
Width="250"
HorizontalAlignment="Left"
IsEnabled="False">
</TextBlock>
<Label x:Name="freeSpaceLabel"
Grid.Column="2"
Grid.Row="2"
Background="WhiteSmoke"
Margin="280,0,0,0"
Width="115"
HorizontalAlignment="Left">
<Run FontFamily="Calibri"
FontWeight="Bold"
FontSize="16"
Text="">
</Run>
</Label>
<Label Grid.Column="2"
Grid.Row="3"
Margin="10,0,10,0"
FontSize="14"
FontWeight="DemiBold"
Content="Select packages to remove from the system"
FontFamily="Calibri">
</Label>
<ListBox x:Name="removePackagesBox"
Background="White"
Grid.Column="2"
Grid.Row="4"
Margin="10,0,10,5">
</ListBox>
<Button Grid.Column="2"
Grid.Row="5"
x:Name="removePackagesButton"
Content="Remove 0 packages (0GB)"
Width="225"
Height="35"
FontSize="16"
Margin="10,10,10,10"
Background="DarkRed"
Foreground="White"
FontFamily="Calibri">
</Button>
</Grid>
</Window>
I create my from, define my TextBoxes, and for my placeholder text I'm using the following code:
$AssetText.Add_MouseClick({ $AssetText.text = “” })
$ErrorText.Add_MouseClick({ $ErrorText.text = “” })
$IssueText.Add_MouseClick({ $IssueText = “” })
$TestTagText.Add_MouseClick({ $TestTagText.text = “” })
$TroubleshootText.Add_MouseClick({ $TroubleshootText.text = “” })
$ResolutionText.Add_MouseClick({ $ResolutionText.text = “” })
It works to remove text from the TextBox, but if I type a fair amount of text in any TextBox and then click outside of it, then come back to it, it erases the text I was working on.
Is there another function I can use that would work better than this current method? So that initially I can click the $TextBox to make the text disappear, but when writing my own text in the box wont disappear after clicking in and outside of the $TextBox?
Here is another approach on setting Placeholder text for Textbox. The approach relies on handling WM_PAINT message and has been explained and implemented here.
The difference between this approach and the other approach (sending EM_SETCUEBANNER) is this approach works for multi-line TextBox as well.
using assembly System.Windows.Forms
using namespace System.Windows.Forms
using namespace System.Drawing
$assemblies = "System.Windows.Forms", "System.Drawing"
$code = #"
using System.Drawing;
using System.Windows.Forms;
public class ExTextBox : TextBox
{
string hint;
public string Hint
{
get { return hint; }
set { hint = value; this.Invalidate(); }
}
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == 0xf)
{
if (!this.Focused && string.IsNullOrEmpty(this.Text)
&& !string.IsNullOrEmpty(this.Hint))
{
using (var g = this.CreateGraphics())
{
TextRenderer.DrawText(g, this.Hint, this.Font,
this.ClientRectangle, SystemColors.GrayText , this.BackColor,
TextFormatFlags.Top | TextFormatFlags.Left);
}
}
}
}
}
"#
#Add the SendMessage function as a static method of a class
Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $code -Language CSharp
# Create an instance of MyForm.
$form = [Form] #{
ClientSize = [Point]::new(400,100);
Text = "Placeholder Sample";
}
$form.Controls.AddRange(#(
($textBox1 = [ExTextBox] #{Location = [Point]::new(10,10); Hint = "Start typing!" })
($textBox2 = [ExTextBox] #{Location = [Point]::new(10,40); Hint = "Start typing!";
MultiLine = $true; Height = 50; })
))
$null = $form.ShowDialog()
$form.Dispose()
If you would like to have placeholder in native OS way, you can send EM_SETCUEBANNER to TextBox to set the placeholder text:
using assembly System.Windows.Forms
using namespace System.Windows.Forms
using namespace System.Drawing
$code = #"
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr SendMessage(IntPtr hWnd,
int msg, IntPtr wParam, string lParam);
public const int EM_SETCUEBANER = 0x1501;
"#
$Win32Helpers = Add-Type -MemberDefinition $code -Name "Win32Helpers" -PassThru
$form = [Form] #{
ClientSize = [Point]::new(400,100);
Text = "Placeholder Text";
}
$form.Controls.AddRange(#(
($textBox1 = [TextBox] #{Location = [Point]::new(10,10) })
($textBox2 = [TextBox] #{Location = [Point]::new(10,40) })
))
$textBox1.add_HandleCreated({
$Win32Helpers::SendMessage($textBox1.Handle,$Win32Helpers::EM_SETCUEBANER`
, [IntPtr]0, "Start typing ...")
$Win32Helpers::SendMessage($textBox2.Handle,$Win32Helpers::EM_SETCUEBANER`
, [IntPtr]0, "Start typing ...")
})
$null = $form.ShowDialog()
$form.Dispose()
When you create a windows form in PowerShell, it will group with the host console window, even if you change the icon for the form.
How do I separate the new form's taskbar icon out from the PowerShell console icon?
What happens on left, desired effect on right:
Example code:
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(300,200)
$form.ShowInTaskbar = $true
$form.Icon = New-Object system.drawing.icon 'c:\icon.ico'
$form.Text = 'New taskbar icon plz'
$form.BringToFront()
$form.ShowDialog()
The only thing I saw that was somewhat helpful was a reference that changing the "Application ID" would separate this out, but the references are all C code.
https://msdn.microsoft.com/en-us/magazine/dd942846.aspx
Please give PowerShell answers, or if the answers include C# code or calls to other APIs, please explain how they work in PowerShell.
After cobbling together a lot of P/Invoke through C# magic from the internet, mostly from here and some from here, I ended up with this to add a type with one method you can use like [PSAppID]::SetAppIdForWindow($form.handle, "Your.AppId"):
$signature = #'
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
public class PSAppID
{
// https://emoacht.wordpress.com/2012/11/14/csharp-appusermodelid/
// IPropertyStore Interface
[ComImport,
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
private interface IPropertyStore
{
uint GetCount([Out] out uint cProps);
uint GetAt([In] uint iProp, out PropertyKey pkey);
uint GetValue([In] ref PropertyKey key, [Out] PropVariant pv);
uint SetValue([In] ref PropertyKey key, [In] PropVariant pv);
uint Commit();
}
// PropertyKey Structure
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct PropertyKey
{
private Guid formatId; // Unique GUID for property
private Int32 propertyId; // Property identifier (PID)
public Guid FormatId
{
get
{
return formatId;
}
}
public Int32 PropertyId
{
get
{
return propertyId;
}
}
public PropertyKey(Guid formatId, Int32 propertyId)
{
this.formatId = formatId;
this.propertyId = propertyId;
}
public PropertyKey(string formatId, Int32 propertyId)
{
this.formatId = new Guid(formatId);
this.propertyId = propertyId;
}
}
// PropVariant Class (only for string value)
[StructLayout(LayoutKind.Explicit)]
public class PropVariant : IDisposable
{
[FieldOffset(0)]
ushort valueType; // Value type
// [FieldOffset(2)]
// ushort wReserved1; // Reserved field
// [FieldOffset(4)]
// ushort wReserved2; // Reserved field
// [FieldOffset(6)]
// ushort wReserved3; // Reserved field
[FieldOffset(8)]
IntPtr ptr; // Value
// Value type (System.Runtime.InteropServices.VarEnum)
public VarEnum VarType
{
get { return (VarEnum)valueType; }
set { valueType = (ushort)value; }
}
public bool IsNullOrEmpty
{
get
{
return (valueType == (ushort)VarEnum.VT_EMPTY ||
valueType == (ushort)VarEnum.VT_NULL);
}
}
// Value (only for string value)
public string Value
{
get
{
return Marshal.PtrToStringUni(ptr);
}
}
public PropVariant()
{ }
public PropVariant(string value)
{
if (value == null)
throw new ArgumentException("Failed to set value.");
valueType = (ushort)VarEnum.VT_LPWSTR;
ptr = Marshal.StringToCoTaskMemUni(value);
}
~PropVariant()
{
Dispose();
}
public void Dispose()
{
PropVariantClear(this);
GC.SuppressFinalize(this);
}
}
[DllImport("Ole32.dll", PreserveSig = false)]
private extern static void PropVariantClear([In, Out] PropVariant pvar);
[DllImport("shell32.dll")]
private static extern int SHGetPropertyStoreForWindow(
IntPtr hwnd,
ref Guid iid /*IID_IPropertyStore*/,
[Out(), MarshalAs(UnmanagedType.Interface)] out IPropertyStore propertyStore);
public static void SetAppIdForWindow(int handle, string AppId)
{
Guid iid = new Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99");
IPropertyStore prop;
int result1 = SHGetPropertyStoreForWindow((IntPtr)handle, ref iid, out prop);
// Name = System.AppUserModel.ID
// ShellPKey = PKEY_AppUserModel_ID
// FormatID = 9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3
// PropID = 5
// Type = String (VT_LPWSTR)
PropertyKey AppUserModelIDKey = new PropertyKey("{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}", 5);
PropVariant pv = new PropVariant(AppId);
uint result2 = prop.SetValue(ref AppUserModelIDKey, pv);
Marshal.ReleaseComObject(prop);
}
}
'#
Add-Type -TypeDefinition $signature
And then with your form:
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(300,200)
$form.ShowInTaskbar = $true
$form.visible = $true
[PSAppID]::SetAppIdForWindow($form.Handle, "YourName.App")
It splits off into its own taskbar entry.
Raymond Chen's blog says the format of the AppID should be "CompanyName.ProductName.SubProduct.VersionInformation where the SubProduct is optional, and the VersionInformation is present only if you want different versions of your app to be treated as distinct." and no longer than 128 characters.
Raymond Chen blog entry 1
Raymond Chen blog entry 2
I think this Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99") is supposed to be a Windows Shell ID, of IPropertyStore, but I don't know how to get it without just writing it in.
Same for the AppUserModelIDKey magic GUID which comes from some Win32 propkey.h maybe?
See also:
https://github.com/microsoft/Windows-classic-samples/blob/7cbd99ac1d2b4a0beffbaba29ea63d024ceff700/Samples/Win7Samples/winui/shell/appshellintegration/AppUserModelIDWindowProperty/AppUserModelIDWindowProperty.cpp#L56
This Microsoft doc which infuriatingly handwaves away as "This is typically IID_IPropertyStore".
AppUserModelID and SystemProperties.System.AppUserModel.ID
I am creating a new question based on my previous post :
Change the Color of Individual ListBox items in Powershell (Winforms)
I have got the following code working very well (thanks to Micky Balladelli) but my boss asked me to add checkboxes. How can I adapt this code to add some checkboxes?
I searched on MSDN in the ListBox Properties and I didn't saw any properties to add checkboxes.
Maybe it would be better to use the CheckedListBox class but in that case it looks complicated to display my colours.
I tried with Listview class but it's not what I am expecting for.
Any help would be appreciated!
function add {
$status='logged1','disconnected1','locked1','logged2','disconnected2','locked2','logged3','disconnected3','locked3'
foreach ($s in $status)
{
$listbox.Items.Add($s)
}
}
$listBox_DrawItem={
param(
[System.Object] $sender,
[System.Windows.Forms.DrawItemEventArgs] $e
)
#Suppose Sender de type Listbox
if ($Sender.Items.Count -eq 0) {return}
#Suppose item de type String
$lbItem=$Sender.Items[$e.Index]
if ( $lbItem.contains('locked'))
{
$Color=[System.Drawing.Color]::yellowgreen
try
{
$brush = new-object System.Drawing.SolidBrush($Color)
$e.Graphics.FillRectangle($brush, $e.Bounds)
}
finally
{
$brush.Dispose()
}
}
$e.Graphics.DrawString($lbItem, $e.Font, [System.Drawing.SystemBrushes]::ControlText, (new-object System.Drawing.PointF($e.Bounds.X, $e.Bounds.Y)))
}
#Generated Form Function
function GenerateForm {
#region Import the Assemblies
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
#endregion
#region Generated Form Objects
$form1 = New-Object System.Windows.Forms.Form
$add = New-Object System.Windows.Forms.Button
$listbox = New-Object System.Windows.Forms.ListBox
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
#endregion Generated Form Objects
#----------------------------------------------
#Generated Event Script Blocks
#----------------------------------------------
#Provide Custom Code for events specified in PrimalForms.
$handler_form1_Load=
{
#TODO: Place custom script here
}
$handler_btnRechercher_Click=
{
add
#TODO: Place custom script here
}
$OnLoadForm_StateCorrection=
{#Correct the initial state of the form to prevent the .Net maximized form issue
$form1.WindowState = $InitialFormWindowState
}
#----------------------------------------------
#region Generated Form Code
$form1.BackColor = [System.Drawing.Color]::FromArgb(255,240,240,240)
$form1.Text = "Move VM"
$form1.Name = "form1"
$form1.AutoScaleMode = 3
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$form1.AutoScroll = $True
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 357
$System_Drawing_Size.Height = 486
$form1.ClientSize = $System_Drawing_Size
$form1.add_Load($handler_form1_Load)
$listbox.FormattingEnabled = $True
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 330
$System_Drawing_Size.Height = 407
$listbox.Size = $System_Drawing_Size
$listbox.DataBindings.DefaultDataSourceUpdateMode = 0
$listbox.Name = "listbox"
$listBox.DrawMode = [System.Windows.Forms.DrawMode]::OwnerDrawFixed
$listBox.Add_DrawItem($listBox_DrawItem)
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 12
$System_Drawing_Point.Y = 21
$listbox.Location = $System_Drawing_Point
$listbox.TabIndex = 4
$listbox.add_Click($action_si_click_sur_VMKO)
$form1.Controls.Add($listbox)
#endregion Generated Form Code
#Save the initial state of the form
$InitialFormWindowState = $form1.WindowState
#Init the OnLoad event to correct the initial state of the form
$form1.add_Load($OnLoadForm_StateCorrection)
#Show the Form
add
$form1.ShowDialog()| Out-Null
} #End Function
#Call the Function
GenerateForm
The trick is to use ListView instead of CheckedListBox.
The following contains the full code with quite a bit of changes.
function add {
$status='logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','logged1','disconnected1','locked1','logged2','disconnected2','locked2','logged3','disconnected3','locked3'
foreach ($s in $status)
{
$i =$listbox.Items.Add($s)
}
}
$listBox_DrawItem={
param(
[System.Object] $sender,
[System.Windows.Forms.DrawListViewItemEventArgs] $e
)
if ( $e.Item.text.contains('locked'))
{
$Color=[System.Drawing.Color]::yellowgreen
try
{
$brush = new-object System.Drawing.SolidBrush($Color)
$e.Graphics.FillRectangle($brush, $e.Bounds)
}
finally
{
$brush.Dispose()
}
}
else
{
$e.DrawBackground();
}
$e.DrawFocusRectangle();
if ($e.Item.Checked)
{
[System.Windows.Forms.ControlPaint]::DrawCheckBox($e.Graphics,$e.Bounds.X, $e.Bounds.Top + 1, 15, 15, [System.Windows.Forms.ButtonState]::Checked)
}
else
{
[System.Windows.Forms.ControlPaint]::DrawCheckBox($e.Graphics,$e.Bounds.X, $e.Bounds.Top + 1, 15, 15, [System.Windows.Forms.ButtonState]::Flat)
}
[System.Drawing.font] $headerFont = new-object System.Drawing.font("Helvetica",
10, [System.Drawing.FontStyle]::Regular)
$sf = new-object System.Drawing.StringFormat
$sf.Alignment = [System.Drawing.StringAlignment]::Near
$rect = New-Object System.Drawing.RectangleF
$rect.x = $e.Bounds.X+16
$rect.y = $e.Bounds.Top+1
$rect.width = $e.bounds.right
$rect.height = $e.bounds.bottom
$e.Graphics.DrawString($e.item.Text,
$headerFont,
[System.Drawing.Brushes]::Black,
$rect,
$sf)
}
$listBox_DrawSubItem={
param(
[System.Object] $sender,
[System.Windows.Forms.DrawListViewSubItemEventArgs] $e
)
}
$listBox_DrawHeader={
param(
[System.Object] $sender,
[System.Windows.Forms.DrawListViewColumnHeaderEventArgs] $e
)
[System.Drawing.font] $headerFont = new-object System.Drawing.font("Helvetica",
10, [System.Drawing.FontStyle]::Bold)
$e.DrawBackground();
$sf = new-object System.Drawing.StringFormat
$sf.Alignment = [System.Drawing.StringAlignment]::Center
$rect = New-Object System.Drawing.RectangleF
$rect.x = $e.Bounds.X+16
$rect.y = $e.Bounds.Top+1
$rect.width = $e.bounds.right
$rect.height = $e.bounds.bottom
$e.Graphics.DrawString($e.Column.text,$e.Item.Font,[System.Drawing.Brushes]::Black, $rect )
$e.Graphics.DrawString($e.Header.Text,
$headerFont,
[System.Drawing.Brushes]::Black,
$rect,
$sf)
}
#Generated Form Function
function GenerateForm {
#region Import the Assemblies
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
#endregion
#region Generated Form Objects
$form1 = New-Object System.Windows.Forms.Form
$add = New-Object System.Windows.Forms.Button
$listbox = New-Object System.Windows.Forms.Listview
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
#endregion Generated Form Objects
#----------------------------------------------
#Generated Event Script Blocks
#----------------------------------------------
#Provide Custom Code for events specified in PrimalForms.
$handler_form1_Load=
{
#TODO: Place custom script here
}
$handler_btnRechercher_Click=
{
add
#TODO: Place custom script here
}
$OnLoadForm_StateCorrection=
{#Correct the initial state of the form to prevent the .Net maximized form issue
$form1.WindowState = $InitialFormWindowState
}
#----------------------------------------------
#region Generated Form Code
$form1.BackColor = [System.Drawing.Color]::FromArgb(255,240,240,240)
$form1.Text = "Move VM"
$form1.Name = "form1"
$form1.AutoScaleMode = 3
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$form1.AutoScroll = $True
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 357
$System_Drawing_Size.Height = 486
$form1.ClientSize = $System_Drawing_Size
$form1.add_Load($handler_form1_Load)
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 330
$System_Drawing_Size.Height = 407
$listbox.Size = $System_Drawing_Size
$listbox.DataBindings.DefaultDataSourceUpdateMode = 0
$listbox.Name = "listview"
$listBox.view = [System.Windows.Forms.View]::Details
$listbox.CheckBoxes = $true
$listbox.fullrowselect = $true
$listBox.OwnerDraw = $true
$listBox.Add_DrawItem($listBox_DrawItem)
$listBox.Add_DrawSubItem($listBox_DrawSubItem)
$listBox.add_DrawColumnHeader($listBox_DrawHeader)
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 12
$System_Drawing_Point.Y = 21
$listbox.Location = $System_Drawing_Point
$listbox.TabIndex = 4
$listbox.add_Click($action_si_click_sur_VMKO)
$listBox.Columns.Add("VMs", 300, [System.Windows.Forms.HorizontalAlignment]::Center) | out-null
$form1.Controls.Add($listbox)
#endregion Generated Form Code
#Save the initial state of the form
$InitialFormWindowState = $form1.WindowState
#Init the OnLoad event to correct the initial state of the form
$form1.add_Load($OnLoadForm_StateCorrection)
#Show the Form
add
$form1.ShowDialog()| Out-Null
} #End Function
#Call the Function
GenerateForm