17

I am trying to display an image via powershell. I made a script based on this forum post.

If I use ShowDialog() it works fine, except the powershell execution stops while the dialog is up. However, that is by design for a modal dialog. If I call Form.Show() in PowershellISE the form shows up, but freezes and cannot be moved or dismissed. Behavior is similar if I copy and past the code to a powershell console.

How do I make the dialog non-modal, and not freeze.

Justin Dearing
  • 14,270
  • 22
  • 88
  • 161
  • 3
    Show() might not be a good idea: http://stackoverflow.com/questions/2192558/why-does-a-form-displayed-by-powershell-sometimes-not-show-up – keyboardP May 13 '11 at 02:46

4 Answers4

21

First Answer Why it appends.

In a Windows graphic program the thread which create a Window must loop in a message pump in order to redistribute (translate) messages coming from the user action to events in his Windows.

In a modal window, the modal code that handles the window display runs its own message pump loop and doesn't return until the window is closed. That's why the code after ShowDialog() won't execute until the window is closed.

Show(), just ask to show the Window, but if there is no pump loop to manage the messages coming from user action, it just freezes.

Second a simple way to have two threads

The CmdLet start-job use another thread from the pool allocated by Powershell so it makes the dialog non-modal, and it does not freeze.

function goForm
{
  [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")

  $file = (get-item 'C:\temp\jpb.png')
  #$file = (get-item "c:\image.jpg")

  $img = [System.Drawing.Image]::Fromfile($file);

  # This tip from http://stackoverflow.com/questions/3358372/windows-forms-look-different-in-powershell-and-powershell-ise-why/3359274#3359274
  [System.Windows.Forms.Application]::EnableVisualStyles();
  $form = new-object Windows.Forms.Form
  $form.Text = "Image Viewer"
  $form.Width = $img.Size.Width;
  $form.Height =  $img.Size.Height;
  $pictureBox = new-object Windows.Forms.PictureBox
  $pictureBox.Width =  $img.Size.Width;
  $pictureBox.Height =  $img.Size.Height;

  $pictureBox.Image = $img;
  $form.controls.add($pictureBox)
  $form.Add_Shown( { $form.Activate() } )
  $form.ShowDialog()
}

Clear-Host

start-job $function:goForm

$name = Read-Host "What is you name"
Write-Host "your name is $name"
JPBlanc
  • 70,406
  • 17
  • 130
  • 175
  • 8
    Ok, for you, knowledge is a sausage, and it's better to keep people ignorant ... you are the architect ... – JPBlanc May 13 '11 at 03:36
  • 9
    x0n What about "that kind of question" makes you think I don't care how the Sausage is made? Yes, in the end I do need to GTD, so in the short term I need start-job or show-ui. However, understanding the "why" helps me evaluate whats going on here. – Justin Dearing May 13 '11 at 11:06
  • 2
    This doesn't work. The thread goForm doesn't show any window, because it's not on the main-thread... – Jomme Sep 30 '19 at 17:00
4

There are ways to make this work, but nothing is worth spending five hours explaining on an open forum. There are other free, shrink-wrapped ways to do this on powershell. Most notably with the free WPF powershell toolkit: Show-UI at http://showui.codeplex.com/ (previously known as WPK and/or PowerBoots - they are merged now.)

x0n
  • 51,312
  • 7
  • 89
  • 111
  • 8
    5min explanation like the one by @JPBlanc is also worthy ;) Why? It is also for other people comming along and trying figure out what's going on. And besides that it can move @Justin further, because he could be curious what all the stuff about message pumping is all about. +1 for both... – stej May 13 '11 at 04:59
3

If your goal is actually to not block the interactive console when an image is shown then you still can use the script as it is with ShowDialog but you should start it using, for example, Start-Job. Thus, the dialog is still modal but it blocks execution in another runspace. The main runspace still can be used for invoking other commands.

Caveats: 1) You should close all opened dialogs before closing the interactive console. 2) If you care, you should remove completed jobs yourself (when a dialog is closed a job that started it still exists).

I use a similar approach in my custom host and it works fine. I also tested it with the script from your link. I changed it slightly so that it is called show-image.ps1 and accepts a file path as a parameter.

This command shows an image and blocks the calling runspace:

show-image.ps1 C:\TEMP\_110513_055058\test.png

This command shows an image and does not block the calling runspace:

Start-Job { show-image.ps1 $args[0] } -ArgumentList C:\TEMP\_110513_055058\test.png
Roman Kuzmin
  • 40,627
  • 11
  • 95
  • 117
  • According to you remark, I just realise that stop-job or remove-job seems to hang on jobs with this king of function. Do you experience the same thing ? – JPBlanc May 13 '11 at 05:37
  • `Stop-Job` finishes only when a dialog has closed. `Remove-Job` works fine after closing a dialog and fails "cannot remove the job ... because the job is not finished" if a dialog is still opened. – Roman Kuzmin May 13 '11 at 05:49
  • `Remove-Job -Force` works but it also "waits" for dialog exit. – Roman Kuzmin May 13 '11 at 05:52
  • Roman, it seems that when the window closes the jobs state becomes "Completed". So when is it necessary to call Stop-Job? is that when you close powershell/powershellISE while the window is opened? – Justin Dearing May 15 '11 at 03:21
  • `Stop-Job` is not needed (besides, it is not quite effective, as we can see). `Remove-Job` might be needed if you care of completed (their windows are closed) jobs that still exist and consume some resources. – Roman Kuzmin May 15 '11 at 06:16
0

Building on @JPBlanc's anwer, this would also be possible (and faster) using a runspace.

Here's a basic example (the rest would basically remain the same)

$ps = [PowerShell]::Create()
[void]$ps.AddScript({
  Add-Type -AssemblyName System.Windows.Forms
  $form = [Windows.Forms.Form]::new()
  $form.ShowDialog()
})
[void]$ps.BeginInvoke()
marsze
  • 15,079
  • 5
  • 45
  • 61