65.9K
CodeProject 正在变化。 阅读更多。
Home

一个 .NET 进度对话框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (56投票s)

2001 年 9 月 27 日

MIT

6分钟阅读

viewsIcon

444924

downloadIcon

10796

用于异步工作程序的进度对话框

Sample Image - Progressdialog.jpg

引言

即使在当今的处理器上,许多处理操作也需要花费超过 1/10 秒才能完成!

为了避免阻塞用户界面,一种常见的方法是启动一个新线程并在那里执行工作,同时将更新发送回 UI 线程上的适当指示器。这是其中一种指示器,使用 .NET 框架用 C# 实现。

第一个有趣的点其实与进度指示器无关,而是一个非常实用的类,它位于 System.Threading 命名空间中,是获取工作在另一个线程上完成的出色的通用方法。它被称为 ThreadPool

private void SpawnSomeWork()
{
    ThreadPool.QueueUserWorkItem( new WaitCallback( DoSomeWork ) );
}

private void DoSomeWork( object status )
{
    ...
}
        

ThreadPool.QueueUserWorkItem 使用 WaitCallback 委托来排队请求 DoSomeWork 操作,以便在运行时线程池中的一个工作线程上执行。对于大多数“发送即忘”的工作者来说,这通常比管理自己的工作线程更简洁。

关于线程池有几个重要的注意事项。最重要的一点是,您 **不** 拥有该线程。您甚至可能不是该线程上执行的唯一工作项。因此,您不应中止或中断线程,或以其他方式干扰其优先级。这意味着您将始终作为后台线程执行,并且您的代码将在线程池中的一个插槽可用时执行。如果这些对您很重要,您应该管理自己的线程。

您会注意到 DoSomeWork 操作接受一个 object 作为参数。这允许您将一些合适的上下文对象传递给您的工作者,稍后我们将对此进行利用,用于我们的进度指示器。

因此,找到了在另一个线程上执行工作的方法后,我们现在需要将进度更新显示在某种 UI 中。为了实现这一点,我创建了一个名为 IProgressCallback 的接口。它定义了工作者可以调用的方法,以在进度指示器进行时更新它,但将该 UI 的实现留作一个单独的问题。

让我们快速看一下那个接口。

/// This defines an interface which can be implemented by UI elements
/// which indicate the progress of a long operation.
/// (See ProgressWindow for a typical implementation)
public interface IProgressCallback
{
  /// Call this method from the worker thread to initialize
  /// the progress callback.
  void Begin( int minimum, int maximum );

  /// Call this method from the worker thread to initialize
  /// the progress callback, without setting the range
  void Begin();

  /// Call this method from the worker thread to reset the range in the 
/// progress callback
void SetRange( int minimum, int maximum ); /// Call this method from the worker thread to update the progress text. void SetText( String text ); /// Call this method from the worker thread to increase the progress
/// counter by a specified value.
void StepTo( int val ); /// Call this method from the worker thread to step the progress meter to a
/// particular value.
void Increment( int val ); /// If this property is true, then you should abort work bool IsAborting { get; } /// Call this method from the worker thread to finalize the progress meter void End(); }

有一些操作允许工作者指示操作的开始 [Begin()] 和结束 [End()],并推进进度指示器本身。有一个实用程序允许您在“进行中”更改指示器的范围 [SetRange()]。此外,还有一个 SetText( String text ) 方法,允许您更新与工作者当前状态关联的通用文本提示。

另一个选择是创建一个工作者需要实现的事件接口。(具体的进度指示器随后可以消耗这些事件并相应地更新它们的 UI)。事件方法的优点是它允许您将进度多路传输给多个订阅者,而无需额外的代码。我更喜欢使用回调方法,然后可能使用事件进行多路传输,一旦所有内容都已重新编排到 UI 线程上(有关更多信息,请参见下文……)

我们现在可以编写一个使用此接口来更新用户进度的用户方法。

private void SpawnSomeWork()
{
    IProgressCallback callback; // = ???
    System.Threading.ThreadPool.QueueUserWorkItem( 
new System.Threading.WaitCallback( DoSomeWork ), callback ); } private void DoSomeWork( object status ) { IProgressCallback callback = status as IProgressCallback; try { callback.Begin( 0, 100 ); for( int i = 0 ; i < 100 ; ++i ) { callback.SetText( String.Format( "Performing op: {0}", i ) ); callback.StepTo( i ); if( callback.IsAborting ) { return; } // Do a block of work if( callback.IsAborting ) { return; } } } catch( System.Threading.ThreadAbortException ) { // We want to exit gracefully here (if we're lucky) } catch( System.Threading.ThreadInterruptedException ) { // And here, if we can } finally { if( callback != null ) { callback.End(); } } }

如上所示,我们在调用 QueueUserWorkItem 时添加了一个 callback 参数。这会被传递到我们的 DoSomeWork 方法中,我们在其中将其从 object 转换回 IProgressCallback

请注意,如果我们没有传递实现 IProgressCallback 的对象,或者传递了 null,则会抛出异常。在生产代码中,我们可以捕获它并进行清理。

同时也要注意我们如何处理终止工作者。我们已经重构了我们的算法,使其能够处理小块的工作,并在每个数据包之后,我们检查 IProgressCallback 是否要求我们中止。如果是,我们需要进行清理并从方法返回。我们还在那里放了几个 catch 块,试图在被中止或中断时干净地退出,尽管在当前版本中,这些操作非常危险,所以我不会抱太大希望!

在处理了客户端之后,我们可以基于此接口实现一个实用的进度指示器对话框。在这种情况下,它被称为 ProgressWindow

本质上,ProgressWindow 是一个包含进度指示器、一个“取消”按钮和一个用于显示进度状态文本的文本窗口的 Form。此外,它还会获取窗口的 Text 属性(即其标题文本),因为它在 Begin() 期间的状态,并使用它作为前缀来更新标题栏中的“已完成百分比”指示器。

公共接口中的每个方法都将从工作线程调用。然而,在窗口中,只有少数方法可以从非拥有 UI 线程调用(例如,CreateGraphics() 就是其中之一),因此我们必须将操作强制回到拥有线程上,以避免灾难。

我们通过使用 Invoke() 方法来实现这一点。

从工作线程调用的每个操作(例如 Begin())都有其对应的 DoBegin() 方法。在实现中,我们调用 Invoke() 并将一个委托传递给 DoBegin() 方法。因此,我们可以依赖 DoBegin() 代码在我们的 UI 线程上执行的事实。

这是 Begin() 方法的一个示例。

/// A delegate for methods which take a range
public delegate void RangeInvoker( int minimum, int maximum );

/// Call this method from the worker thread to initialize
/// the progress meter.
public void Begin( int minimum, int maximum )
{
    initEvent.WaitOne();
    Invoke( new RangeInvoker( DoBegin ), new object[] { minimum, maximum } );
}

private void DoBegin( int minimum, int maximum )
{
    DoBegin();
    DoSetRange( minimum, maximum );
}

private void DoBegin()
{
    cancelButton.Enabled = true;
    ControlBox = true;
}

private void DoSetRange( int minimum, int maximum )
{
    progressBar.Minimum = minimum;
    progressBar.Maximum = maximum;
    progressBar.Value = minimum;
    titleRoot = Text;
}
        

关于这一点有几点需要注意。首先,请注意参数如何通过 Invoke() 调用作为 object 数组传递给 DoBegin(),然后在 DoBegin() 本身中正确匹配到实际的方法参数。

我们还通过等待一个由 FormLoad 处理程序发出的事件来阻止工作线程,以确保在工作者开始之前显示 UI(因此可以更新或关闭)。

所有其余方法都遵循类似的模式,最终结果是一个跨线程的进度指示对话框。

通常,客户端会以模态(使用 ShowDialog())或非模态(使用 Show())方式调用它。在所有其他方面,它可以被当作常规窗体处理。您可以设置背景颜色、图像,并调整其大小(控件的默认锚定将使文本框和进度滑块扩展以适应)。

希望您觉得它有用。

历史

2001 年 10 月 2 日 - 更新了源文件以修复一些问题。

2003 年 8 月 24 日 - 更新了源文件和文章,以解决 ThreadAbort() 的不当行为,并建议应该可以在“进行中”重置范围。

© . All rights reserved.