English 中文(简体)
Creating a progress bar that runs on another thread, while keeping calculation in main thread
原标题:

Preface: I know this is an unusual/improper way to do this. I can do this with a "real" ShowDialog(), background worker/thread, and so on. I m not looking for help doing it that way; I am trying to do specifically what I describe here, even if it is ugly. If this is impossible for X reason, please let me know though.


I have created a fancy progress dialog for some of our long running operations. I need to have this dialog shown on a new thread while having processing continue on the calling (UI in most cases) thread.

This has 3 real requirements:

  • Prevent user interaction with the calling form (similar to ShowDialog(this))
  • Keep the progress dialog above the main window (it can fall behind now)
  • Allow the main thread to continue processing

What I have looks like this (and works just fine so far, as far as running goes, except for those issues above):

Using ... ShowNewProgressDialogOnNewThread() ...
      Logic
      UpdateProgress() //static
      Logic
      UpdateProgress() //static, uses Invoke() to call dialog
      ...
End Using  // destroys the form, etc

I have tried a few ways to do this:

  • ShowDialog() on BackgroundWorker / Thread
  • Action.BeginInvoke() which calls a function
  • ProgressForm.BeginInvoke(... method that calls ShowDialog... )
  • Wrapping main form in a class that implements IWin32Window so it can be called cross-threaded and passed to ShowDialog() - this one failed somewhere later one, but at least causes ShowDialog() to not barf immediately.

Any clues or wisdom on how to make this work?

Solution (For Now)

  • The call to EnableWindow is what did what I was looking for.
  • I do not experience any crashes at all
  • Changed to use ManualResetEvent
  • I set TopMost, because I couldn t always guarantee the form would end up on top otherwise. Perhaps there is a better way.
  • My progress form is like a splash screen (no sizing, no toolbar, etc), perhaps that accounts for the lack of crashes (mentioned in answer)
  • Here is another thread on the EnableWindow topic (didn t reference for this fix, tho)
最佳回答

Getting the progress window consistently displayed on top of the (dead) form is the difficult requirement. This is normally handled by using the Form.Show(owner) overload. It causes trouble in your case, WF isn t going to appreciate the owner form belonging to another thread. That can be worked around by P/Invoking SetWindowLong() to set the owner.

But now a new problem emerges, the progress window goes belly-up as soon as it tries to send a message to its owner. Somewhat surprisingly, this problem kinda disappears when you use Invoke() instead of BeginInvoke() to update progress. Kinda, you can still trip the problem by moving the mouse over the border of the disabled owner. Realistically, you ll have to use TopMost to nail down the Z-order. More realistically, Windows just doesn t support what you are trying to do. You know the real fix, it is at the top of your question.

Here s some code to experiment with. It assumes you progress form is called dlgProgress:

Imports System.Threading

Public Class ShowProgress
  Implements IDisposable
  Private Delegate Sub UpdateProgressDelegate(ByVal pct As Integer)
  Private mOwnerHandle As IntPtr
  Private mOwnerRect As Rectangle
  Private mProgress As dlgProgress
  Private mInterlock As ManualResetEvent

  Public Sub New(ByVal owner As Form)
    Debug.Assert(owner.Created)
    mOwnerHandle = owner.Handle
    mOwnerRect = owner.Bounds
    mInterlock = New ManualResetEvent(False)
    Dim t As Thread = New Thread(AddressOf dlgStart)
    t.SetApartmentState(ApartmentState.STA)
    t.Start()
    mInterlock.WaitOne()
  End Sub

  Public Sub Dispose() Implements IDisposable.Dispose
    mProgress.BeginInvoke(New MethodInvoker(AddressOf dlgClose))
  End Sub

  Public Sub UpdateProgress(ByVal pct As Integer)
    mProgress.Invoke(New UpdateProgressDelegate(AddressOf dlgUpdate), pct)
  End Sub

  Private Sub dlgStart()
    mProgress = New dlgProgress
    mProgress.StartPosition = FormStartPosition.Manual
    mProgress.ShowInTaskbar = False
    AddHandler mProgress.Load, AddressOf dlgLoad
    AddHandler mProgress.FormClosing, AddressOf dlgClosing
    EnableWindow(mOwnerHandle, False)
    SetWindowLong(mProgress.Handle, -8, mOwnerHandle)
    Application.Run(mProgress)
  End Sub

  Private Sub dlgLoad(ByVal sender As Object, ByVal e As EventArgs)
    mProgress.Location = New Point( _
      mOwnerRect.Left + (mOwnerRect.Width - mProgress.Width)  2, _
      mOwnerRect.Top + (mOwnerRect.Height - mProgress.Height)  2)
    mInterlock.Set()
  End Sub

  Private Sub dlgUpdate(ByVal pct As Integer)
    mProgress.ProgressBar1.Value = pct
  End Sub

  Private Sub dlgClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs)
    EnableWindow(mOwnerHandle, True)
  End Sub

  Private Sub dlgClose()
    mProgress.Close()
    mProgress = Nothing
  End Sub

   --- P/Invoke
  Public Shared Function SetWindowLong(ByVal hWnd As IntPtr, ByVal nIndex As Integer, ByVal dwNewLong As IntPtr) As IntPtr
    If IntPtr.Size = 4 Then
      Return SetWindowLongPtr32(hWnd, nIndex, dwNewLong)
    Else
      Return SetWindowLongPtr64(hWnd, nIndex, dwNewLong)
    End If
  End Function

  Private Declare Function EnableWindow Lib "user32.dll" (ByVal hWnd As IntPtr, ByVal enabled As Boolean) As Boolean
  Private Declare Function SetWindowLongPtr32 Lib "user32.dll" Alias "SetWindowLongW" (ByVal hWnd As IntPtr, ByVal nIndex As Integer, ByVal dwNewLong As IntPtr) As IntPtr
  Private Declare Function SetWindowLongPtr64 Lib "user32.dll" Alias "SetWindowLongW" (ByVal hWnd As IntPtr, ByVal nIndex As Integer, ByVal dwNewLong As IntPtr) As IntPtr

End Class

Sample usage:

  Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    Using dlg As New ShowProgress(Me)
      For ix As Integer = 1 To 100
        dlg.UpdateProgress(ix)
        System.Threading.Thread.Sleep(50)
      Next
    End Using
  End Sub
问题回答

I know it s a bit dirty but can t you just do the work in the dialog??

I mean something like

Dialog.MyShowDialog(callback);

and do all the work in callback as well as the UI update.

That way you ll retain the ShowDialog behaivour while allowing different code to be called.

I wrote a blog post on this topic a while ago (dealing with splash forms, but the idea is the same). The code is in C#, but I will try to convert it an post it here (coming...).





相关问题
Bring window to foreground after Mutex fails

I was wondering if someone can tell me what would be the best way to bring my application to the foreground if a mutex was not able to be created for a new instance. E.g.: Application X is running ...

How to start WinForm app minimized to tray?

I ve successfully created an app that minimizes to the tray using a NotifyIcon. When the form is manually closed it is successfully hidden from the desktop, taskbar, and alt-tab. The problem occurs ...

Linqy no matchy

Maybe it s something I m doing wrong. I m just learning Linq because I m bored. And so far so good. I made a little program and it basically just outputs all matches (foreach) into a label control. ...

Handle DataTable.DataRow cell change event

I have a DataTable that has several DataColumns and DataRow. Now i would like to handle an event when cell of this DataRow is changed. How to do this in c#?

Apparent Memory Leak in DataGridView

How do you force a DataGridView to release its reference to a bound DataSet? We have a rather large dataset being displayed in a DataGridView and noticed that resources were not being freed after the ...

ALT Key Shortcuts Hidden

I am using VS2008 and creating forms. By default, the underscore of the character in a textbox when using an ampersand is not shown when I run the application. ex. "&Goto Here" is not ...

WPF-XAML window in Winforms Application

I have a Winforms application coded in VS C# 2008 and want to insert a WPF window into the window pane of Winforms application. Could you explain me how this is done.

热门标签