Again, thanks for the help with my previous question, but i have hit another problem. As mentioned above the exe in the below code if run in cmd instantly outputs progress as it runs.
However the textbox in the form is blank and there is a delay, where it looks like nothing is happening before the whole output is pasted into the box.
I have looked online and mention of a forms.application do events method but it is not recommended and a bit sloppy.
Any ideas how i could have this live?. I did try a messagebox but i need to close it before the exe would run and i would still have to wait.
I'm referring to the textbox output from xtract-iso.exe in xiso_build function
Code:
Function xiso_build {
Set-Location -Path $PSScriptRoot # change to root folder of this script wherever it's run from
[System.Windows.Forms.Messagebox]::Show("Building, Please Wait...")
$outputBox.text= & .\extract-xiso.exe -r $selected_file 2>&1 | out-string # '2>&1' needs to be there otherwise any errors get outputted to terminal, out-string for better formatting
}
##########################################################################################################################
# the main form
$form = New-Object System.Windows.Forms.Form
$form.StartPosition = 'CenterScreen'
$form.Text = 'Xbox Iso Extractor'
$form.Size = '600,600'
# Choose iso label
# Create a "computer name" label control and add it to the form
# Set label location, text, size, etc
$Label1 = New-Object Windows.Forms.Label
$label1.Font = [System.Drawing.Font]::new("Microsoft Sans Serif", 12, [System.Drawing.FontStyle]::Bold)
$Label1.Size = '180,40'
$Label1.Location = '10,20'
$Label1.Text = "Select An Xbox ISO:"
$Label1.Font.Bold
$form.Controls.Add($Label1)
# textbox
$isotextBox = New-Object System.Windows.Forms.TextBox
$isotextBox.Location = '10,60'
$isotextBox.Size = '320,200'
$form.Controls.Add($isotextBox)
# open file button
$Select_Iso_button = New-Object System.Windows.Forms.button
$Select_Iso_button.Text = 'Choose ISO'
$Select_Iso_button.Size = '100,25'
$Select_Iso_button.Location = '350,60'
$form.controls.Add($Select_Iso_button)
# below code: on click run 'iso_open func above and run global '$selected_file_path' variable from fun, then insert path and file into textbox
# save this selected text into var called $selected_file then execute var
$Select_Iso_button.Add_Click({iso_open; $global:selected_file = $isotextBox.Text = $selected_file_path; $selected_file})
# Output of xtract-iso textbox
$outputBox = New-Object System.Windows.Forms.TextBox #creating the text box
$outputBox.Location = '10,150' #location of the text box (px) in relation to the primary window's edges (length, height)
$outputBox.Size = New-Object System.Drawing.Size(565,200) #the size in px of the text box (length, height)
$outputBox.MultiLine = $True #declaring the text box as multi-line
$outputBox.ScrollBars = "Vertical" #adding scroll bars if required
$form.Controls.Add($outputBox) #activating the text box inside the primary window
# Build Iso Button
$build_button = New-Object System.Windows.Forms.button
$build_button.Text = 'Build ISO'
$build_button.Size = '200,50'
$build_button.Location = '10,360'
# $button.Anchor = 'Bottom,left' # uncomment to move button down to bottom left of app window
$form.Controls.Add($build_button)
$build_button.Add_Click({xiso_build}) # run 'xiso_build' func from above
CodePudding user response:
By default, Out-String
collects all input first before outputting the input objects' formatted representations as a single, multiline string.[1]
In order to get near-realtime feedback from the command getting executed, you need to append lines to $output.Text
iteratively, as they become available, which you can do via a ForEach-Object
call:
$sep = ''; $outputBox.text = ''
& .\extract-xiso.exe -r $selected_file 2>&1 |
ForEach-Object {
# Append the line at hand to the text box.
$outputBox.AppendLine($sep $_); $sep = "`n"
# Keep the form responsive - THIS MAY NOT BE ENOUGH - see below.
[System.Windows.Forms.Application]::DoEvents()
}
[System.Windows.Forms.Application]::DoEvents()
[2] is used to keep the form responsive while the command is executing, but note that this relies on successive output lines generated by your .\extract-xiso.exe
call being emitted in relatively short succession (the reason is that the form cannot process any user events while PowerShell is waiting for the next output line to be emitted).
If this doesn't keep your form responsive enough, you'll need a different approach, such as using a background job to run your executable, and using .Show()
instead of .ShowDialog()
to show your form, with a subsequent, manual [System.Windows.Forms.Application]::DoEvents()
loop that periodically polls the background job for new output lines.
The following simplified, self-contained example demonstrates this approach:
Add-Type -AssemblyName System.Windows.Forms
# === Helper functions:
# Launches the external program in a a background job and store
# the job-inf variable in script-level variable $job.
function Start-ExtractionJob {
$script:job = Start-Job {
# Simulate a long-running call to an external program.
# Here is where you'd make your & .\extract-xiso.exe ... call
1..20 | % { cmd /c "echo line $_" 2>&1; Start-Sleep 1 }
}
}
# Adds an output line to the $ouputBox text box
function Add-TextBoxLine {
param([string] $line)
if ($prevLen = $outputBox.Text.Length) {
$sep = "`r`n"
}
else {
$sep = ''
}
$outputBox.AppendText($sep $line)
# Scroll to the *start* of the new line to prevent horizontal scrolling.
$outputBox.Select($prevLen $sep.Length, 0)
}
# === Create the form.
$form = [System.Windows.Forms.Form] @{
StartPosition = 'CenterScreen'
Text = 'Xbox Iso Extractor'
Size = '600,600'
}
# Mutiline text box that will receive the program's output, line by line.
$outputBox = [System.Windows.Forms.TextBox] @{
Location = '10,150'
Size = [System.Drawing.Size]::new(565, 200)
MultiLine = $true
ScrollBars = "Vertical" #adding scroll bars if required
}
$form.Controls.Add($outputBox)
# Build Iso Button
$build_button = [System.Windows.Forms.Button] @{
Text = 'Build ISO'
Size = '200,50'
Location = '10,360'
}
$form.Controls.Add($build_button)
# Set up the button event handler for launching
# the external program in a background job.
$build_button.Add_Click({
$this.Enabled = $false
$outputBox.Clear()
Add-TextBoxLine "=== Launching..."
Start-ExtractionJob
})
# --- Manage display of the form and handle job output.
$job = $null
$form.Show() # Show form asynchronously.
while ($form.Visible) {
# While the form is visible, process events periodically.
[System.Windows.Forms.Application]::DoEvents()
# If a job has been launched, collect its output on an ongoing basis.
# If the job has completed, remove it, and reenable the launch button.
if ($job) {
$job | Receive-Job | ForEach-Object {
# $outputBox.Text = $nl $_; $outputBox.Select($outputBox.Text.Length $nl.Length, 0); $outputBox.ScrollToCaret()
Add-TextBoxLine $_
}
# Check if the job has terminated, for whatever reason.
if ($job.State -in 'Completed', 'Failed') {
$job | Remove-Job; $job = $null
Add-TextBoxLine '=== Completed'
$build_button.Enabled = $true
}
# Sleep a little.
Start-Sleep -Milliseconds 100
}
}
# ... clean up job, if necessary, ...
[1] While there is a -Stream
switch that outputs the lines of the formatted representations one by one, that wouldn't help here, because the key to realtime feedback is to assign to $output.Text
iteratively, as the lines become available.
[2] [System.Windows.Forms.Application]::DoEvents()
can be problematic in general (it is essentially what the blocking .ShowDialog()
call does in a loop behind the scenes), but in this constrained scenario (assuming only one form is to be shown) it should be fine. See this answer for background information.