Vb.net 创建在另一个线程上运行的进度条,同时将计算保留在主线程中

Vb.net 创建在另一个线程上运行的进度条,同时将计算保留在主线程中,vb.net,winforms,multithreading,dialog,Vb.net,Winforms,Multithreading,Dialog,前言:我知道这是一种不寻常/不恰当的做法。我可以使用“真正的”ShowDialog()、后台工作线程/线程等来实现这一点。我不是在寻求帮助,而是这样做;我试图具体地做我在这里描述的事情,即使它是丑陋的。如果这是不可能的X原因,请让我知道虽然 我为一些长期运行的操作创建了一个奇特的进度对话框。我需要在新线程上显示此对话框,同时在调用(大多数情况下是UI)线程上继续处理 这有三个真正的要求: 防止用户与调用表单交互(类似于ShowDialog(此)) 将进度对话框保持在主窗口上方(它现在可能会落

前言:我知道这是一种不寻常/不恰当的做法。我可以使用“真正的”ShowDialog()、后台工作线程/线程等来实现这一点。我不是在寻求帮助,而是这样做;我试图具体地做我在这里描述的事情,即使它是丑陋的。如果这是不可能的X原因,请让我知道虽然


我为一些长期运行的操作创建了一个奇特的进度对话框。我需要在新线程上显示此对话框,同时在调用(大多数情况下是UI)线程上继续处理

这有三个真正的要求:

  • 防止用户与调用表单交互(类似于ShowDialog(此))
  • 将进度对话框保持在主窗口上方(它现在可能会落后)
  • 允许主线程继续处理
我所做的看起来是这样的(到目前为止,就运行而言,效果还不错,除了上面提到的那些问题):

我尝试了几种方法来做到这一点:

  • BackgroundWorker/Thread上显示对话框
  • Action.BeginInvoke()调用函数的
  • ProgressForm.BeginInvoke(…调用ShowDialog的方法…)
  • 将主窗体包装到实现IWin32Window的类中,以便可以跨线程调用它并将其传递给ShowDialog()-此操作在稍后的某个地方失败,但至少不会导致ShowDialog()立即停止
关于如何做这件事有什么线索或智慧吗

解决方案(目前)
  • 对EnableWindow的调用正是我所寻找的
  • 我根本没有经历过任何撞车事故
  • 更改为使用ManualResetEvent
  • 我设置了TopMost,因为我不能保证表单最终会位于顶部。也许有更好的办法
  • 我的进度表就像一个启动屏幕(没有大小,没有工具栏等),也许这就是为什么没有崩溃的原因(在回答中提到)
  • 以下是(此修复程序没有参考,tho)

我不久前写过(处理splash表单,但想法是一样的)。代码是C#,但我将尝试将其转换为post it here(即将发布…。

我知道它有点脏,但你不能在对话框中完成工作吗

我是说

Dialog.MyShowDialog(callback);
并在回调和UI更新中完成所有工作


这样,您将保留ShowDialog行为,同时允许调用不同的代码。

让进度窗口始终显示在(死)窗体的顶部是一个困难的要求。这通常通过使用Form.Show(owner)重载来处理。它会给您带来麻烦,WF不会欣赏属于另一个线程的所有者表单。这可以通过调用SetWindowLong()设置所有者来解决

但现在出现了一个新的问题,进度窗口在试图向其所有者发送消息时就会崩溃。令人惊讶的是,当您使用Invoke()而不是BeginInvoke()来更新进度时,这个问题就消失了。在某种程度上,您仍然可以通过将鼠标移到残疾所有者的边界上来解决问题。实际上,您必须使用最上面的来确定Z顺序。更现实地说,Windows不支持您正在尝试的操作。你知道真正的解决办法,这是你问题的首要问题

这里有一些代码可以进行实验。它假定您的进度表称为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
示例用法:

  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

这与我现在使用显式线程(新线程(AddressOf….SetApptState().Start())所做的工作具有类似的效果。问题是,我需要在后台设置一个窗口来阻止与之交互,就像真正的ShowDialog()一样。目前,我可以点击BG中的窗口,它将获得焦点,将进度窗口放在后台。我可以,但这不是我想为这个对话框做的。在C#中,这更容易,我可以使用lambduh,让编译器为我完成提升变量的艰苦工作。但是目标是团队中的vb开发人员,包括我自己,所以这在4.0之前是不可能的。我把你的用户名作为一个承诺:)说真的,谢谢你详细介绍为什么它不起作用。我想我会有一段艰难的时光,但对于我的消费者来说,使用现有代码会更容易。希望能把所有硬的东西放在一个地方。。我在看另外一个提到调用“EnableWindow”的东西。。我们来看看结果如何。到目前为止,我已经把它做好了!是EnableDisable调用实现了这一点。我也改为使用ManualResetEvent,而不是一个变量,我甚至不知道这个。非常有用!我正在试图理解de代码。调用SetWindowLong时,将传递-8索引值。-8的意思是什么。在msdn上找不到此值。确定,我发现-8是GWL\U HWNDPARENT。因此,调用SetWindowLong将为dilgProgress设置父进程。一个问题是为什么调用SetWindowLong而不是SetParent?
  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