I need to interact with a IE window that was previously created in PowerShell without using a global variable.
If(WindowAlreadyCreatedByPowerShell){
$IE = WindowAlreadyCreatedByPowerShell
}Else{
$IE = New-Object -com internetexplorer.application;
$IE.visible = $true;
$IE.navigate($url);
}
Note: Using ((New-Object -ComObject Shell.Application).Windows).Invoke() | ?{$_.Name -eq "Internet Explorer"} returns nothing
So you will need to store something in the parent scope - if you don't want to store the IE object, store the URL you opened, or even better, the window handle (HWND property, which should be unique). Otherwise, you won't have a reliable way of getting which window you want, especially if your session is opening other IE windows as well.
# Variable for the created IE window handle
$expectedHWND = $null
# This will enumerate all ShellWindows opened as a COM object in your session
$allShellWindows = ( New-Object -ComObject Shell.Application ).Windows()
# Get the IE object matching the HWND you stored off when you first opened IE
# If you opened other windows in the session, the HWND should be unique so you
# can get the correct window you are expecting
$existingIE = $allShellWindows | Where-Object {
$_.HWND -eq $expectedHWND
}
# Your code, slightly modified
if( $existingIE ){
# You should be able to operate on $existingIE here instead of reassigning it to $IE
Write-Output "Found existing IE session: HWND - $($existingIE.HWND), URL - $($existingIE.LocationURL)"
} else {
$IE = New-Object -com internetexplorer.application;
$IE.visible = $true;
$IE.navigate($url);
$expectedHWND = $IE.HWND
}
If you run that twice, but don't set $expectedHWND to $null the second time, it will find your IE window. Note that this is a lot of work for avoiding storing a variable in the parent scope, but is technically feasible.
You will likely need to curate the sample above for your specific application, but serves as a good demonstration of getting an operable object for the IE window you previously opened.
Related
I start building up a small winform with Copy button and a label under this button.
When I click on Copy button it starts to copy files from source to destination.
I would like to run this asynchroniously so I don't want form to be freezed while copy operation runs. That's why I use Job. After a successful copy I need feedback of copy and show an "OK" text with green color but it is not working.
Here is my code:
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[System.Windows.Forms.Application]::EnableVisualStyles()
Function Copy-Action{
$Computername = "testclient"
$Source_Path = "C:\temp\"
$Destination_Path = "\\$Computername\c$\temp"
$job = Start-Job -Name "Copy" -ArgumentList $Source_Path,$Destination_Path –ScriptBlock {
param($Source_Path,$Destination_Path)
Copy-Item $Source_Path -Destination $Destination_Path -Recurse -Force
}
Register-ObjectEvent $job StateChanged -MessageData $Status_Label -Action {
[Console]::Beep(1000,500)
$Status_Label.Text = "OK"
$Status_Label.ForeColor = "#009900"
$eventSubscriber | Unregister-Event
$eventSubscriber.Action | Remove-Job
} | Out-Null
}
# DRAW FORM
$form_MainForm = New-Object System.Windows.Forms.Form
$form_MainForm.Text = "Test Copy"
$form_MainForm.Size = New-Object System.Drawing.Size(200,200)
$form_MainForm.FormBorderStyle = "FixedDialog"
$form_MainForm.StartPosition = "CenterScreen"
$form_MainForm.MaximizeBox = $false
$form_MainForm.MinimizeBox = $true
$form_MainForm.ControlBox = $true
# Copy Button
$Copy_Button = New-Object System.Windows.Forms.Button
$Copy_Button.Location = "50,50"
$Copy_Button.Size = "75,30"
$Copy_Button.Text = "Copy"
$Copy_Button.Add_Click({Copy-Action})
$form_MainForm.Controls.Add($Copy_Button)
# Status Label
$Status_Label = New-Object System.Windows.Forms.Label
$Status_Label.Text = ""
$Status_Label.AutoSize = $true
$Status_Label.Location = "75,110"
$Status_Label.ForeColor = "black"
$form_MainForm.Controls.Add($Status_Label)
#show form
$form_MainForm.Add_Shown({$form_MainForm.Activate()})
[void] $form_MainForm.ShowDialog()
Copy is successful but showing an "OK" label won't. I have placed a Beep but it doesn't work too.
What am I doing wrong ? Any solution to this?
Thank you.
Let me offer alternatives to balrundel's helpful solution - which is effective, but complex.
The core problem is that while a form is being shown modally, with .ShowDialog(), WinForms is in control of the foreground thread, not PowerShell.
That is, PowerShell code - in the form's event handlers - only executes in response to user actions, which is why your job-state-change event handler passed to Register-ObjectEvent's -Action parameter does not fire (it would eventually fire, after closing the form).
There are two fundamental solutions:
Stick with .ShowDialog() and perform operations in parallel, in a different PowerShell runspace (thread).
balrundel's solution uses the PowerShell SDK to achieve this, whose use is far from trivial, unfortunately.
See below for a simpler alternative based on Start-ThreadJob
Show the form non-modally, via the .Show() method, and enter a loop in which you can perform other operations while periodically calling [System.Windows.Forms.Application]::DoEvents() in order to keep the form responsive.
See this answer for an example of this technique.
A hybrid approach is to stick with .ShowDialog() and enter a [System.Windows.Forms.Application]::DoEvents() loop inside the form event handler.
This is best limited to a single event handler applying this technique, as using additional simultaneous [System.Windows.Forms.Application]::DoEvents() loops invites trouble.
See this answer for an example of this technique.
Simpler, Start-ThreadJob-based solution:
Start-ThreadJob is part of the the ThreadJob module that offers a lightweight, thread-based alternative to the child-process-based regular background jobs and is also a more convenient alternative to creating runspaces via the PowerShell SDK.
It comes with PowerShell (Core) 7+ and can be installed on demand in Windows PowerShell with, e.g., Install-Module ThreadJob -Scope CurrentUser.
In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.
In addition to syntactic convenience, Start-ThreadJob, due to being thread-based (rather than using a child process, which is what Start-Job does), allows manipulating the calling thread's live objects.
Note that the sample code below, in the interest of brevity, performs no explicit thread synchronization, which may situationally be required.
The following simplified, self-contained sample code demonstrates the technique:
The sample shows a simple form with a button that starts a thread job, and updates the form from inside that thread job after the operation (simulated by a 3-second sleep) completes, as shown in the following screen shots:
Initial state:
After pressing Start Job (the form remains responsive):
After the job has ended:
The .add_Click() event handler contains the meat of the solution; the source-code comments hopefully provide enough documentation.
# PSv5+
using namespace System.Windows.Forms
using namespace System.Drawing
Add-Type -AssemblyName System.Windows.Forms
# Create a sample form.
$form = [Form] #{
Text = 'Form with Thread Job'
ClientSize = [Point]::new(200, 80)
FormBorderStyle = 'FixedToolWindow'
}
# Create the controls and add them to the form.
$form.Controls.AddRange(#(
($btnStartJob = [Button] #{
Text = "Start Job"
Location = [Point]::new(10, 10)
})
[Label] #{
Text = "Status:"
AutoSize = $true
Location = [Point]::new(10, 40)
Font = [Font]::new('Microsoft Sans Serif', 10)
}
($lblStatus = [Label] #{
Text = "(Not started)"
AutoSize = $true
Location = [Point]::new(80, 40)
Font = [Font]::new('Microsoft Sans Serif', 10)
})
))
# The script-level helper variable that maintains a collection of
# thread-job objects created in event-handler script blocks,
# which must be cleaned up after the form closes.
$script:jobs = #()
# Add an event handler to the button that starts
# the background job.
$btnStartJob.add_Click( {
$this.Enabled = $false # To prevent re-entry while the job is still running.
# Signal the status.
$lblStatus.Text = 'Running...'
$form.Refresh() # Update the UI.
# Start the thread job, and add the job-info object to
# the *script-level* $jobs collection.
# The sample job simply sleeps for 3 seconds to simulate a long-running operation.
# Note:
# * The $using: scope is required to access objects in the caller's thread.
# * In this simple case you don't need to maintain a *collection* of jobs -
# you could simply discard the previous job, if any, and start a new one,
# so that only one job object is ever maintained.
$script:jobs += Start-ThreadJob {
# Perform the long-running operation.
Start-Sleep -Seconds 3
# Update the status label and re-enable the button.
($using:lblStatus).Text = 'Done'
($using:btnStartJob).Enabled = $true
}
})
$form.ShowDialog()
# Clean up the collection of jobs.
$script:jobs | Remove-Job -Force
Start-Job creates a separate process, and when your form is ready to receive events, it can't listen to job events. You need to create a new runspace, which is able to synchronize thread and form control.
I adapted code from this answer. You can read much better explanation there.
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[System.Windows.Forms.Application]::EnableVisualStyles()
Function Copy-Action{
$SyncHash = [hashtable]::Synchronized(#{TextBox = $Status_Label})
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ThreadOptions = "UseNewThread"
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash)
$Worker = [PowerShell]::Create().AddScript({
$SyncHash.TextBox.Text = "Copying..."
# Copy-Item
$Computername = "testclient"
$Source_Path = "C:\temp\"
$Destination_Path = "\\$Computername\c$\temp"
Copy-Item $Source_Path -Destination $Destination_Path -Recurse -Force
$SyncHash.TextBox.ForeColor = "#009900"
$SyncHash.TextBox.Text = "OK"
})
$Worker.Runspace = $Runspace
$Worker.BeginInvoke()
}
# DRAW FORM
$form_MainForm = New-Object System.Windows.Forms.Form
$form_MainForm.Text = "Test Copy"
$form_MainForm.Size = New-Object System.Drawing.Size(200,200)
$form_MainForm.FormBorderStyle = "FixedDialog"
$form_MainForm.StartPosition = "CenterScreen"
$form_MainForm.MaximizeBox = $false
$form_MainForm.MinimizeBox = $true
$form_MainForm.ControlBox = $true
# Copy Button
$Copy_Button = New-Object System.Windows.Forms.Button
$Copy_Button.Location = "50,50"
$Copy_Button.Size = "75,30"
$Copy_Button.Text = "Copy"
$Copy_Button.Add_Click({Copy-Action})
$form_MainForm.Controls.Add($Copy_Button)
# Status Label
$Status_Label = New-Object System.Windows.Forms.Label
$Status_Label.Text = ""
$Status_Label.AutoSize = $true
$Status_Label.Location = "75,110"
$Status_Label.ForeColor = "black"
$form_MainForm.Controls.Add($Status_Label)
#show form
$form_MainForm.Add_Shown({$form_MainForm.Activate()})
[void] $form_MainForm.ShowDialog()
I have a powershell script I'm running on startup to open a website and log in. It works fine whenever I run the script manually, or whenever I sign out of my profile in Windows and sign back in. However, when I restart the computer, the script opens the website, but then fails to edit the DOM to change the username and password text fields and click the submit button. It seems to attempt to change the values because the cursor stops blinking, but nothing happens.
Here is the code.
$IEProcess = [System.Diagnostics.Process]::Start("iexplore", "-k https://www.website.com")
Sleep -Seconds 1
$IE = $(New-Object -ComObject "Shell.Application").Windows() | ? {$_.HWND -eq $IEProcess.MainWindowHandle}
while ($IE.ReadyState -ne 4)
{
Start-Sleep -Milliseconds 100
}
$IE.Document.getElementById(“userNameInput”).value = $Username
$IE.Document.getElementByID(“passwordInput”).value= $Password
$IE.Document.getElementById(“submitButton”).Click()
Using this code, I was able to get it to work. It may have to do with how you are configuring it to run on startup.
$Url = "https://stackoverflow.com/users/login?ssrc=head&returnurl=https%3a%
2f%2fstackoverflow.com%2f"
$Username="name#email.com"
$Password="password"
$IE = New-Object -com internetexplorer.application;
$IE.visible = $true;
$IE.navigate($url);
while ($IE.Busy -eq $true)
{
Start-Sleep -Milliseconds 2000;
}
$IE.Document.getElementById("email").value = $Username
$IE.Document.getElementByID("password").value=$Password
$IE.Document.getElementById("submit-button").Click()
I did the following:
Open gpedit.msc
User config > Windows Settings > Scripts (Logon/Logoff)
"PowerShell Scripts" tab
Add script
I need the script to run at specific times, gets the system time to determine which url to open, takes the screenshot, saves it and send it via mail.
If i break the code down portions of it run but when i include the time checks and if statement it doesn't run.
Here is the script i came up with but its not running.
Function Get-shot {
$now = (Get-Date -Format "hh:mm:ss")
$ie=new-object -com "InternetExplorer.Application"
$ie.Visible=$true
$path="C:\checks"
Function takeshot {
$ScreenBounds = [Windows.Forms.SystemInformation]::VirtualScreen
$ScreenshotObject = New-Object Drawing.Bitmap
$ScreenBounds.Width,
$ScreenBounds.Height
$DrawingGraphics = [Drawing.Graphics]::FromImage($ScreenshotObject)
$DrawingGraphics.CopyFromScreen($ScreenBounds.Location, [Drawing.Point]::Empty, $ScreenBounds.Size)
$DrawingGraphics.Dispose()
$ScreenshotObject.Save($FilePath)
$ScreenshotObject.Dispose()
}
Try {
Add-Type -Assembly System.Windows.Forms
If ($now -eq "08:28:00")
{
<#opens internet explorer and navigates to the page#>
$ie.navigate("www.google.com")
$Time = (Get-Date)
[string] $FileName = "$($Time.Month)"
$FileName += '-'
$FileName += "$($Time.Day)"
$FileName += '-'
$FileName += "$($Time.Year)"
$FileName += '-'
$FileName += '.png'
[string] $FilePath = (Join-Path $Path $FileName)
#takes the screenshot
DO {
takeshot
Write-Verbose "Saved screenshot to $FilePath."
} while ($now -eq "08:28:30")
}
} Catch {
Write-Warning "$Error[0].ToString() +
$Error[0].InvocationInfo.PositionMessage"
}
}
You don't state where this will be run. Meaning:
On a server / workstation you control, and are interactively logged
on to.
On a server / workstation you control and you are not interactively
logged on to.
On a server / workstation you control, that someone else is logged
on to.
You don't say if this is to be a scheduled task or an event monitor.
You don't say or show how the target URL is determined. So, is it
embedded, pulled from some list, or user input.
You are calling the bitmap type but specifying .png extension.
You don't handle multi-monitor in your code.
There are several things that are not correct in your posted code.
File paths do not match up. You have $path is some place
The nested function declaration is not really needed based on what you are after.
The way you are calling the object for the bitmap is not correct, as it will generate errors, which you don't show in your code.
WARNING: A constructor was not found. Cannot find an appropriate
constructor for type Drawing.Bitmap. A constructor was not found.
Cannot find an appropriate constructor for type Drawing.Bitmap. You
cannot call a method on a null-valued expression. You cannot call a
method on a null-valued expression. Exception calling "FromImage" with
"1" argument(s): "Value cannot be null.
I am not sure why the need for the Do/While, since you need only to
call the take shot code once.
Your code will not start at any predetermined time, unless you have
a scheduled task or some other event trigger to start it.
An for this...
but when i include the time checks and if statement it doesn't run.
Your code assigns a definitive time.
If ($now -eq "08:28:00")
If it is not that exact time, when you run the code, the code will not run, because you told it not to, if it is not 08:28:00".
If you are running this throughout the day for your testing. Change the above to this...
If ($now -ge "08:28:00")
Yet, again, this does not really make since. As I stated earlier, unless this code is on some type of trigger, WMI event, Scheduled Task, already running in memory via a infinite loop (not a good thing BTW, well, you could set a timed cycled one), then it is not going to fire anyway.
Walkthrough (you'll have to wrap your logic around this example - including any multi-monitor stuff)
Clear-Host
$StartTime = $null
$TargetUrl = $null
$FileName = $null
$File = $null
# script to run at specific times
## Set up a schedueld task for the script run cycle(s)
### Scheduled Task (set script run daily at "08:28:00") )
### WMI Event Monitor (check for clock time of "08:28:00"))
### Load the script on boot up and use a timed infinite loop (Load at boot, Loop un time time of "08:28:00" - need to reload script each day to handle if use never reboots)
# determine which url to open
# How the URL is determined
$TargetUrl = "google.com"
# Get-Content -Path "D:\Temp\UrlList.txt" | Get-Random -Count 1
# $ResponseUrl = (Read-Host -Prompt "Enter the URL to use")
# Open the IE to the needed URL
$ie = new-object -com "InternetExplorer.Application"
$ie.navigate($TargetUrl)
$ie.visible = $true
$ie.fullscreen = $False
While ($ie.Busy) {Start-Sleep -Milliseconds 100}
# take the screenshot and saves it
Function New-ScreenShot
{
[CmdletBinding()]
[Alias('nss')]
Param
(
# [Parameter(Mandatory = $true)]
[string]$Path = "$env:USERPROFILE\Pictures"
)
$FileName = "$($env:COMPUTERNAME)_$(get-date -f yyyyMMddHHmmss).bmp"
$File = "$Path\$FileName"
Add-Type -AssemblyName System.Windows.Forms
Add-type -AssemblyName System.Drawing
# Gather Screen resolution information
$Screen = [System.Windows.Forms.SystemInformation]::VirtualScreen
$Width = $Screen.Width
$Height = $Screen.Height
$Left = $Screen.Left
$Top = $Screen.Top
# Create bitmap using the top-left and bottom-right bounds
$bitmap = New-Object System.Drawing.Bitmap $Width, $Height
# Create Graphics object
$graphic = [System.Drawing.Graphics]::FromImage($bitmap)
# Capture screen
$graphic.CopyFromScreen($Left, $Top, 0, 0, $bitmap.Size)
# Save to file
$bitmap.Save($File)
Write-Output "Screenshot saved to:"
Write-Output $File
Start-Sleep -Seconds 1
Start-Process -FilePath mspaint -ArgumentList $File
}
New-ScreenShot
# send it via mail
# from the PoSH built-in help file
# Example 2: Send an attachment
Send-MailMessage -From "User01 <user01#example.com>" `
-To "User02 <user02#example.com>", "User03 <user03#example.com>" `
-Subject "Sending the URL Screenshot Attachment" `
-Body "Forgot to send the attachment. Sending now." `
-Attachments $File -Priority Low -dno onSuccess, onFailure -SmtpServer "smtp.example.com"
Update
Relative to the the OP comments
– athanas otieno
Sure you can do different types.
What I provided was the basics to do this. There are far more elegant options.
There are plenty of other samples to be leveraged that show .png or .jpg use case. These are all over the web and even on this very site. See these posts.
How can I do a screen capture in Windows PowerShell?
Get-Screenshot.ps1
<#
.Synopsis
Gets a screen capture
.Description
Captures the current screen
.Example
# Capture the whole screen
Get-ScreenShot
.Example
# Capture the current window
Get-ScreenShot -OfWindow
.Example
# Capture a set of coordinates
Get-ScreenShot -OfCoordinates 320, 240
.Link
http://stackoverflow.com/questions/2969321/how-can-i-do-a-screen-capture-in-windows-powershell
#>
# The image format used to store the screen capture
[Parameter(ValueFromPipelineByPropertyName=$true)]
[ValidateSet('PNG', 'JPEG', 'TIFF', 'GIF', 'BMP')]
[string]
$ImageFormat = 'JPEG', https://www.powershellgallery.com/packages/RoughDraft/0.1/Content/Get-Screenshot.ps1
Take-Screenshot This script has a function that allows you to take a
screenshot of the entire desktop or of an active window. Also includes
option to save the screenshot to a file.
Take-ScreenShot -screen -file "C:\image.png" -imagetype png
https://gallery.technet.microsoft.com/scriptcenter/eeff544a-f690-4f6b-a586-11eea6fc5eb8/view/Discussions
Even this script appears to be where you got yours from, but I could be wrong, as often it's really easy to see folks to end up similar things. It has happen to me many times. Of course forking / borrowing and tweaking is a thing.
Changing a screenshot to JPG instead of PNG
I've got a script that works that runs on a scheduled task to snapshot
the screen of a computer. Basically the computer runs a query at a
regular interval that is pretty resource intensive on the server but
has information that anyone might want to see at a given time.
https://social.technet.microsoft.com/Forums/windowsserver/en-US/c121b827-44c8-49ac-83c6-356fa720e169/changing-a-screenshot-to-jpg-instead-of-png?forum=winserverpowershell
Im using windows Windows.Forms.RichTextBox to redirect my powershell script output "$var".
Detect.Urls is already enabled and working, but unable to open them by clicking.
Can any one help me with the code for link click event handler in powershell script........
$outputBox = New-Object System.Windows.Forms.RichTextBox
$outputBox.Location = New-Object System.Drawing.Size(10,150)
$outputBox.Size = New-Object System.Drawing.Size(700,300)
$outputBox.MultiLine = $True
$outputBox.SelectionIndent = 8
$outputBox.SelectionHangingIndent = 3
$outputBox.SelectionRightIndent = 12
$outputBox.ScrollBars = "ForcedBoth"
$Form.Controls.Add($outputBox)
$outputBox.Text = $var
$Form.Add_Shown({$Form.Activate()})
[void] $Form.ShowDialog()
You have to handle click events by yourself
$outputBox.add_LinkClicked({
Start-Process -FilePath $_.LinkText
})
Will open link in the default browser when clicked.
This is how I do it with Powershell Studio...right click the control and add a new event...add the linkclicked event and then go to the script and add the following.
depending on what you want to be opened you may need to used something other than explorer, but the $_.linktext should have the link you want from the url. note if there are spaces you will have to something to replace them as the url will be broke at the first space it encounters.
$Linkclicked = $_.LinkText
explorer.exe $Linkclicked
I play around with Power Shell... And I have a newbie question to navigate with ie.
Have this Code:
# IE window
$ie = New-Object -com "InternetExplorer.Application"
$ie.visible = $true
function waitforpageload {
while ($ie.Busy -eq $true) { Start-Sleep -Milliseconds 1000; }
}
# navigate to the first page
$ie.Navigate("http://ss64.com/bash/")
waitforpageload
$ie.Document.Url # return http://ss64.com/bash/
# navigate to the second page
$ie.Navigate("http://ss64.com/ps/")
waitforpageload
$ie.Document.Url # return also http://ss64.com/bash/
and I'm wondering why $ie.Document.Url in both times return http://ss64.com/bash/
should is it possible to get http://ss64.com/ps/ in the second call?
Thanks a lot
You'll get the current location using
$ie.LocationURL
To get a list of all methods and properties available, use $ie | gm