【问题标题】:Creating a progress bar that runs on another thread, while keeping calculation in main thread创建一个在另一个线程上运行的进度条,同时在主线程中保持计算
【发布时间】:2010-12-16 02:20:39
【问题描述】:

前言:我知道这是一种不寻常/不正确的做法。我可以使用“真实”的 ShowDialog()、后台工作程序/线程等来做到这一点。我不是在寻求帮助。我正在尝试专门做我在这里描述的事情,即使它很丑陋。如果由于 X 原因这是不可能的,请告诉我。


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

这有 3 个真正的要求:

  • 阻止用户与调用表单交互(类似于 ShowDialog(this))
  • 将进度对话框保持在主窗口上方(现在可能会落后)
  • 允许主线程继续处理

我所拥有的看起来像这样(到目前为止运行良好,除了上面的那些问题):

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

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

  • BackgroundWorker/线程
  • 上的ShowDialog()
  • Action.BeginInvoke() 调用函数
  • ProgressForm.BeginInvoke(...调用 ShowDialog... 的方法...)
  • 将主窗体包装在一个实现 IWin32Window 的类中,以便可以跨线程调用它并传递给 ShowDialog() - 这个在稍后的某个地方失败了,但至少导致 ShowDialog() 不会立即出错。

关于如何完成这项工作的任何线索或智慧?

解决方案(暂时)

  • 对 EnableWindow 的调用正是我所寻找的。
  • 我完全没有遇到任何崩溃
  • 改为使用 ManualResetEvent
  • 我设置了 TopMost,因为我不能总是保证表单最终会在最上面。也许有更好的方法。
  • 我的进度表就像一个闪屏(没有调整大小、没有工具栏等),这可能是导致没有崩溃的原因(在回答中提到)
  • 这里是 another thread on the EnableWindow topic(虽然没有参考此修复程序)

【问题讨论】:

    标签: vb.net winforms multithreading dialog


    【解决方案1】:

    我前一阵子写了a blog post on this topic(处理飞溅形式,但想法是一样的)。代码在 C# 中,但我会尝试将其转换为在此处发布(即将发布...)。

    【讨论】:

    • 这与我现在使用显式线程 (New Thread(AddressOf ...).SetApptState().Start()) 所做的效果相似。问题是我在后台有一个窗口,我需要防止与之交互,就像真正的 ShowDialog() 会做的那样。目前我可以点击BG中的窗口,它将获得焦点,将进度窗口置于后台。
    【解决方案2】:

    我知道这有点脏,但你不能只做对话框中的工作吗??

    我的意思是像

    Dialog.MyShowDialog(callback);
    

    并在回调和 UI 更新中完成所有工作。

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

    【讨论】:

    • 我可以,但这不是我想要为这个对话框做的。
    • 这在 C# 中要容易得多,我可以使用 lambduh 并让编译器为我完成提升变量的艰苦工作。但目标是团队中的 vb 开发人员,包括我自己,所以在 4.0 之前是不行的。
    【解决方案3】:

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

    但是现在出现了一个新问题,进度窗口在尝试向其所有者发送消息时立即崩溃。有点令人惊讶的是,当您使用 Invoke() 而不是 BeginInvoke() 来更新进度时,这个问题就消失了。有点,您仍然可以通过将鼠标移到残疾所有者的边界上来解决问题。实际上,您必须使用 TopMost 来确定 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
    

    【讨论】:

    • 我将您的用户名视为承诺 :) 说真的,感谢您提供有关它为什么不起作用的详细信息。我想我会很难过,但使用现有代码对我的消费者来说会更容易。希望把所有困难的东西放在一个地方。
    • 嘿..我在看其他提到调用“EnableWindow”的东西..我们会看看情况如何。
    • 到目前为止,我让它运行良好!是 EnableDisable 调用做到了。我也改用 ManualResetEvent,而不是变量,我什至不知道那个。
    • 非常有帮助!我试图理解解码。当调用 SetWindowLong 时,传递了一个 -8 索引值。 -8 是什么意思。在 msdn 上没有找到这个值。
    • 好吧,我发现 -8 是 GWL_HWNDPARENT。所以调用 SetWindowLong 将为 dilgProgress 设置父级。一个问题为什么要调用 SetWindowLong 而不是 SetParent?
    猜你喜欢
    • 1970-01-01
    • 2018-07-03
    • 1970-01-01
    • 2013-08-19
    • 1970-01-01
    • 2012-06-22
    • 2019-08-16
    • 1970-01-01
    • 2012-03-28
    相关资源
    最近更新 更多