Создание индикатора выполнения, выполняемого в другом потоке, с сохранением расчетов в основном потоке - PullRequest
5 голосов
/ 13 ноября 2009

Предисловие: Я знаю, что это необычный / неподходящий способ сделать это. Я могу сделать это с помощью «реального» ShowDialog (), фоновый рабочий / поток и так далее. Я не ищу помощи, делающей это таким образом; Я пытаюсь сделать именно то, что я описываю здесь, даже если это некрасиво. Если это невозможно по причине Х, пожалуйста, дайте мне знать.


Я создал фантастический диалог прогресса для некоторых из наших длительных операций. Мне нужно, чтобы это диалоговое окно отображалось в новом потоке, а обработка продолжалась в вызывающем потоке (в большинстве случаев в пользовательском интерфейсе).

Это имеет 3 реальных требования:

  • Запретить взаимодействие пользователя с формой вызова (аналогично ShowDialog (this))
  • Сохранить диалог прогресса над главным окном (теперь оно может отставать)
  • Разрешить основному потоку продолжить обработку

То, что у меня есть, выглядит следующим образом (и до сих пор работает отлично, насколько работает, за исключением тех проблем, описанных выше):

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

Я попробовал несколько способов сделать это:

  • ShowDialog () на BackgroundWorker / Тема
  • Action.BeginInvoke () , которая вызывает функцию
  • ProgressForm.BeginInvoke ( ... метод, который вызывает ShowDialog ...)
  • Обертывание главной формы в классе, который реализует IWin32Window, так что его можно назвать перекрестным и передать в ShowDialog () - этот сбой где-то позже, но, по крайней мере, заставляет ShowDialog () немедленно не блокировать.

Любые подсказки или мудрость о том, как сделать эту работу?

Решение (На данный момент)

  • Вызов EnableWindow - это то, что я искал.
  • У меня вообще нет сбоев
  • Изменено для использования ManualResetEvent
  • Я установил TopMost, потому что не всегда мог гарантировать, что форма окажется сверху. Возможно, есть лучший способ.
  • Моя форма прогресса похожа на заставку (без изменения размеров, без панели инструментов и т. Д.), Возможно, это объясняет отсутствие сбоев (упомянуто в ответе)
  • Вот другой поток в теме EnableWindow (не ссылался на это исправление, хотя)

Ответы [ 3 ]

6 голосов
/ 13 ноября 2009

Получение окна прогресса, последовательно отображаемого поверх (мертвой) формы, является сложным требованием. Обычно это выполняется с помощью перегрузки Form.Show (владельца). В вашем случае это вызывает проблемы, 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
1 голос
/ 13 ноября 2009

Я знаю, что это немного грязно, но вы не можете просто сделать работу в диалоге ??

Я имею в виду что-то вроде

Dialog.MyShowDialog(callback);

и выполнять всю работу в обратном вызове, а также в обновлении пользовательского интерфейса.

Таким образом вы сохраните поведение ShowDialog, одновременно позволяя вызывать другой код.

1 голос
/ 13 ноября 2009

Я написал сообщение в блоге на эту тему некоторое время назад (имеет дело с заставками, но идея та же).Код написан на C #, но я постараюсь преобразовать его в сообщение здесь (будет ...).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...