From 1de87c2c00be8a0784399c3afcc534f7b6db9b5b Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Tue, 4 Dec 2007 20:54:42 +0000 Subject: [PATCH] Added a tool strip host for the tracking bar, allowing it to be embedded inside a status bar; fixed a bug in the progress reporter form that would prevent AsyncAbort() from actually being called when the user clicked on the cancel button; AsyncProgressBar no longer changes the style of the progress bar, this is now up to the user; ProgressReporterForm now switches ProgressBar between Marquee and Blocks styles on its own; various formatting enhancements git-svn-id: file:///srv/devel/repo-conversion/nuwi@13 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Windows.Forms.csproj | 5 + Source/AsyncProgressBar/AsyncProgressBar.cs | 45 +++++---- Source/ContainerListView/ContainerListView.cs | 5 +- .../ProgressReporter/ProgressReporterForm.cs | 25 ++++- Source/TrackingBar/ToolStripTrackingBar.cs | 92 +++++++++++++++++++ Source/TrackingBar/TrackingBar.cs | 12 +-- 6 files changed, 158 insertions(+), 26 deletions(-) create mode 100644 Source/TrackingBar/ToolStripTrackingBar.cs diff --git a/Nuclex.Windows.Forms.csproj b/Nuclex.Windows.Forms.csproj index f0212e4..cc9db0a 100644 --- a/Nuclex.Windows.Forms.csproj +++ b/Nuclex.Windows.Forms.csproj @@ -104,6 +104,11 @@ false ProgressReporterForm.Designer + + false + ToolStripTrackingBar + Component + false TrackingBar diff --git a/Source/AsyncProgressBar/AsyncProgressBar.cs b/Source/AsyncProgressBar/AsyncProgressBar.cs index b623253..3990a11 100644 --- a/Source/AsyncProgressBar/AsyncProgressBar.cs +++ b/Source/AsyncProgressBar/AsyncProgressBar.cs @@ -11,10 +11,17 @@ namespace Nuclex.Windows.Forms { /// Progress bar with optimized multi-threading behavior /// - /// If a background thread is generating lots of progress updates, using synchronized - /// calls can drastically reduce performance. This progress bar optimizes this case - /// by performing the update asynchronously and keeping only the most recent update - /// when multiple updates arrive while the asynchronous update call is still running. + /// + /// If a background thread is generating lots of progress updates, using synchronized + /// calls can drastically reduce performance. This progress bar optimizes that case + /// by performing the update asynchronously and keeping only the most recent update + /// when multiple updates arrive while the asynchronous update call is still running. + /// + /// + /// This design eliminates useless queueing of progress updates, thereby reducing + /// CPU load occuring in the UI thread and at the same time avoids blocking the + /// worker thread, increasing its performance. + /// /// public partial class AsyncProgressBar : ProgressBar { @@ -22,8 +29,8 @@ namespace Nuclex.Windows.Forms { public AsyncProgressBar() { InitializeComponent(); - this.updateProgressDelegate = new MethodInvoker(updateProgress); this.Disposed += new EventHandler(progressBarDisposed); + this.updateProgressDelegate = new MethodInvoker(updateProgress); // Could probably use VolatileWrite() as well, but for consistency reasons // this is an Interlocked call, too. Mixing different synchronization measures @@ -37,6 +44,11 @@ namespace Nuclex.Windows.Forms { /// Not used private void progressBarDisposed(object sender, EventArgs arguments) { + // CHECK: This method is only called on an explicit Dispose() of the control. + // Microsoft officially states that it's allowed to call Control.BeginInvoke() + // without calling Control.EndInvoke(), so this code is quite correct, + // but is it also clean? :> + // Since this has to occur in the UI thread, there's no way that updateProgress() // could be executing just now. But the final call to updateProgress() will not // have EndInvoke() called on it yet, so we do this here before the control @@ -46,15 +58,18 @@ namespace Nuclex.Windows.Forms { this.progressUpdateAsyncResult = null; } - // CHECK: This method is only called on an explicit Dispose() of the control. - // Microsoft officially states that it's allowed to call Control.BeginInvoke() - // without calling Control.EndInvoke(), so this code is quite correct, - // but is it also clean? :> - } /// Asynchronously updates the value to be shown in the progress bar /// New value to set the progress bar to + /// + /// This will schedule an asynchronous update of the progress bar in the UI thread. + /// If you change the progress value again before the progress bar has completed its + /// update cycle, the original progress value will be skipped and the progress bar + /// jumps directly to the latest progress value. Updates are not queued, there is + /// at most one update waiting on the UI thread. It is also strictly guaranteed that + /// the last most progress value set will be shown and never skipped. + /// public void AsyncSetValue(float value) { // Update the value to be shown on the progress bar. If this happens multiple @@ -79,10 +94,6 @@ namespace Nuclex.Windows.Forms { /// Synchronously updates the value visualized in the progress bar private void updateProgress() { - // Switch the style if the progress bar is still set to marquee mode - if(Style == ProgressBarStyle.Marquee) - Style = ProgressBarStyle.Blocks; - // Cache these to shorten the code that follows :) int minimum = base.Minimum; int maximum = base.Maximum; @@ -93,8 +104,10 @@ namespace Nuclex.Windows.Forms { // invocation to ensure the most recent value will remain at the end. float progress = Interlocked.Exchange(ref this.newProgress, -1.0f); - // Convert the value to the progress bar's configured range and assign it - // to the progress bar + // Restrain the value to the progress bar's configured range and assign it. + // This is done to prevent exceptions in the UI thread (theoretically the user + // could change the progress bar's min and max just before the UI thread executes + // this method, so we cannot validate the value in AsyncSetValue()) int value = (int)(progress * (maximum - minimum)) + minimum; base.Value = Math.Min(Math.Max(value, minimum), maximum); diff --git a/Source/ContainerListView/ContainerListView.cs b/Source/ContainerListView/ContainerListView.cs index 8032439..8b9df59 100644 --- a/Source/ContainerListView/ContainerListView.cs +++ b/Source/ContainerListView/ContainerListView.cs @@ -36,7 +36,7 @@ namespace Nuclex.Windows.Forms { /// embedded controls seperate of the ListView's items. The first option /// would require a complete rewrite of the ListViewItem class and its related /// support classes, all of which are surprisingly large and complex. Thus, - /// the less clean but more doable latter option has been chosen. + /// I chose the less clean but more doable latter option. /// public partial class ContainerListView : System.Windows.Forms.ListView { @@ -45,14 +45,17 @@ namespace Nuclex.Windows.Forms { this.embeddedControlClickedHandler = new EventHandler(embeddedControlClicked); this.embeddedControls = new ListViewEmbeddedControlCollection(); + this.embeddedControls.Added += new EventHandler( embeddedControlAdded ); + this.embeddedControls.Removed += new EventHandler( embeddedControlRemoved ); + this.embeddedControls.Clearing += new EventHandler(embeddedControlsClearing); diff --git a/Source/ProgressReporter/ProgressReporterForm.cs b/Source/ProgressReporter/ProgressReporterForm.cs index 7f82494..826160f 100644 --- a/Source/ProgressReporter/ProgressReporterForm.cs +++ b/Source/ProgressReporter/ProgressReporterForm.cs @@ -101,7 +101,8 @@ namespace Nuclex.Windows.Forms { Text = windowTitle; // Only enable the cancel button if the progression can be aborted - this.cancelButton.Enabled = (progression is IAbortable); + this.abortReceiver = (progression as IAbortable); + this.cancelButton.Enabled = (this.abortReceiver != null); // Subscribe the form to the progression it is supposed to monitor progression.AsyncEnded += this.asyncEndedDelegate; @@ -144,7 +145,19 @@ namespace Nuclex.Windows.Forms { /// Contains the new progress achieved by the progression /// private void asyncProgressUpdated(object sender, ProgressUpdateEventArgs arguments) { + + // See if this is the first progress update we're receiving. If yes, we need to + // switch the progress bar from marquee into its normal mode! + int haveProgress = Interlocked.Exchange(ref this.areProgressUpdatesIncoming, 1); + if(haveProgress == 0) { + this.progressBar.BeginInvoke( + (MethodInvoker)delegate() { this.progressBar.Style = ProgressBarStyle.Blocks; } + ); + } + + // Send the new progress to the progress bar this.progressBar.AsyncSetValue(arguments.Progress); + } /// @@ -176,8 +189,8 @@ namespace Nuclex.Windows.Forms { if(this.abortReceiver != null) { - // Do this first because the abort receiver might trigger the AsyncEnded - // event in the calling thread and thus destroy our window even in + // Do this first because the abort receiver might trigger the AsyncEnded() + // event in the calling thread (us!) and thus destroy our window even in // the safe and synchronous UI thread :) this.cancelButton.Enabled = false; @@ -200,6 +213,12 @@ namespace Nuclex.Windows.Forms { /// 2: Ready to close and close requested, triggers close /// private int state; + /// Whether we're receiving progress updates from the progression + /// + /// 0: No progress updates have arrived so far + /// 1: We have received at least one progress update from the progression + /// + private int areProgressUpdatesIncoming; /// /// If set, reference to an object implementing IAbortable by which the /// ongoing background process can be aborted. diff --git a/Source/TrackingBar/ToolStripTrackingBar.cs b/Source/TrackingBar/ToolStripTrackingBar.cs new file mode 100644 index 0000000..c66aaae --- /dev/null +++ b/Source/TrackingBar/ToolStripTrackingBar.cs @@ -0,0 +1,92 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +using Nuclex.Support.Tracking; + +namespace Nuclex.Windows.Forms { + + /// Tracking bar that can be hosted in a tool strip container + public class ToolStripTrackingBar : ToolStripControlHost { + + /// Initializes a new tool strip tracking bar + public ToolStripTrackingBar() : base(createTrackingBar()) { + hideControlAtRuntime(); + } + + /// Initializes a new tool strip tracking bar with a name + /// Name of the tracking bar control + public ToolStripTrackingBar(string name) : base(createTrackingBar(), name) { + hideControlAtRuntime(); + } + + /// The tracking bar control being hosted by the tool strip host + public TrackingBar TrackingBarControl { + get { return base.Control as TrackingBar; } + } + + /// Tracks the specified progression in the tracking bar + /// Progression to be tracked + public void Track(Progression progression) { + TrackingBarControl.Track(progression); + } + + /// Tracks the specified progression in the tracking bar + /// Progression to be tracked + /// Weight of this progression in the total progress + public void Track(Progression progression, float weight) { + TrackingBarControl.Track(progression, weight); + } + + /// Stops tracking the specified progression + /// Progression to stop tracking + public void Untrack(Progression progression) { + TrackingBarControl.Untrack(progression); + } + + /// Default size of the hosted control + protected override Size DefaultSize { + get { return new Size(100, 15); } + } + + /// Default margin to leave around the control in the tool strip + protected override Padding DefaultMargin { + get { + if((base.Owner != null) && (base.Owner is StatusStrip)) + return new Padding(1, 3, 1, 3); + + return new Padding(1, 2, 1, 1); + } + } + + /// Creates a new tracking bar + /// A new tracking bar + private static TrackingBar createTrackingBar() { + TrackingBar trackingBar = new TrackingBar(); + trackingBar.Size = new Size(100, 15); + return trackingBar; + } + + /// Hides the control during runtime usage + private void hideControlAtRuntime() { + TrackingBarControl.VisibleChanged += new EventHandler(trackingBarVisibleChanged); + + LicenseUsageMode usageMode = System.ComponentModel.LicenseManager.UsageMode; + if(usageMode == LicenseUsageMode.Runtime) + base.Visible = false; + } + + /// + /// Toggles the visibility of the tool strip host when the tracking bar control's + /// visibility changes. + /// + /// Tracking bar control whose visiblity has changed + /// Not used + private void trackingBarVisibleChanged(object sender, EventArgs e) { + base.Visible = TrackingBarControl.Visible; + } + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/TrackingBar/TrackingBar.cs b/Source/TrackingBar/TrackingBar.cs index c951435..53eeaaa 100644 --- a/Source/TrackingBar/TrackingBar.cs +++ b/Source/TrackingBar/TrackingBar.cs @@ -40,12 +40,7 @@ namespace Nuclex.Windows.Forms { // We start off being in the idle state (and thus, being invisible) this.isIdle = true; - base.Visible = false; - - // Create the tracker and attach ourselfes to its events - this.tracker = new ProgressionTracker(); - this.tracker.AsyncIdleStateChanged += this.asyncIdleStateChangedDelegate; - this.tracker.AsyncProgressUpdated += this.asyncProgressUpdateDelegate; + this.Visible = false; // Initialize the delegates we use to update the control's state and those // we use to register ourselfes to the tracker's events @@ -56,6 +51,11 @@ namespace Nuclex.Windows.Forms { this.asyncProgressUpdateDelegate = new EventHandler( asyncProgressUpdated ); + + // Create the tracker and attach ourselfes to its events + this.tracker = new ProgressionTracker(); + this.tracker.AsyncIdleStateChanged += this.asyncIdleStateChangedDelegate; + this.tracker.AsyncProgressUpdated += this.asyncProgressUpdateDelegate; } /// Tracks the specified progression in the tracking bar