using System; using System.Data; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace Nuclex.Windows.Forms.Controls { /// Displays a progress spinner to entertain the user while waiting public partial class ProgressSpinner : UserControl { /// Number of dots the progress spinner will display private const int DotCount = 8; /// Size of a normal dot (only ever assumed by the trailing dot) private const int DotRadius = 4; /// /// The leading dot will be DotCount times this larger than a normal dot /// private const int ScaleFactor = 1; /// Initializes a new progress spinner public ProgressSpinner() { SetStyle( ( ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint | ControlStyles.SupportsTransparentBackColor ), true ); InitializeComponent(); Disposed += new EventHandler(OnDisposed); if(!DesignMode) { StartSpinner(); } } /// Releases all resources owned by the control when it is destroyed /// Control that is being destroyed /// Not used private void OnDisposed(object sender, EventArgs arguments) { if(this.dotOutlinePen != null) { this.dotOutlinePen.Dispose(); this.dotOutlinePen = null; } if(this.dotFillBrush != null) { this.dotFillBrush.Dispose(); this.dotFillBrush = null; } } /// Starts the spinner's animation public void StartSpinner() { this.spinnerRunning = true; this.animationUpdateTimer.Enabled = true; } /// Stops the spinner's animation public void StopSpinner() { this.animationUpdateTimer.Enabled = false; this.spinnerRunning = false; } /// Color used to fill the dots public Color DotFillColor { get { return this.dotFillColor; } set { if(value != this.dotFillColor) { this.dotFillColor = value; if(this.dotFillBrush != null) { this.dotFillBrush.Dispose(); this.dotFillBrush = null; } } } } /// Color used for the dots' outline public Color DotOutlineColor { get { return this.dotOutlineColor; } set { if(value != this.dotOutlineColor) { this.dotOutlineColor = value; if(this.dotOutlinePen != null) { this.dotOutlinePen.Dispose(); this.dotOutlinePen = null; } } } } /// Font that is used to display the status text public Font StatusFont { get { return this.statusFont; } set { this.statusFont = value; } } /// Text that will be displayed as the control's status public string StatusText { get { return this.statusText; } set { this.statusText = value; } } /// Called when the control is hidden or shown /// Not used protected override void OnVisibleChanged(EventArgs arguments) { base.OnVisibleChanged(arguments); this.animationUpdateTimer.Enabled = this.spinnerRunning && Visible; } /// Called when the control should redraw itself /// Provides access to the drawing surface and tools protected override void OnPaint(PaintEventArgs arguments) { paintControlsBehindMe(arguments); paintAnimatedDots(arguments); paintStatusMessage(arguments); } /// Forcefully redraws the controls below this one /// Provides access to the drawing surface and tools /// /// /// WinForms has very poor transparency support. A transparent control will only /// be transparent to its immediate parent (so the parent needs to be a container /// control and hold the transparent control as its preferrably only child). /// /// /// Worse yet, if you manually establish this relationship in your .Designer.cs /// file, the Visual Studio WinForms designer will dismantle it next time you /// edit something. This method fixes those issues by repainting all controls /// that are behind this control and whose bounding box intersect this control. /// /// private void paintControlsBehindMe(PaintEventArgs arguments) { if(Parent != null && this.BackColor == Color.Transparent) { using(var bmp = new Bitmap(Parent.Width, Parent.Height)) { Parent.Controls.Cast() .Where(c => Parent.Controls.GetChildIndex(c) > Parent.Controls.GetChildIndex(this)) .Where(c => c.Bounds.IntersectsWith(this.Bounds)) .OrderByDescending(c => Parent.Controls.GetChildIndex(c)) .ToList() .ForEach(c => c.DrawToBitmap(bmp, c.Bounds)); arguments.Graphics.DrawImage(bmp, -Left, -Top); } } } /// Draws a simple animated dots animation /// Provides access to the drawing surface and tools private void paintAnimatedDots(PaintEventArgs arguments) { if(this.dotOutlinePen == null) { this.dotOutlinePen = new Pen(this.dotOutlineColor); } if(this.dotFillBrush == null) { this.dotFillBrush = new SolidBrush(this.dotFillColor); } SmoothingMode prevousSmoothingMode = arguments.Graphics.SmoothingMode; arguments.Graphics.SmoothingMode = SmoothingMode.HighQuality; try { PointF center = new PointF(Width / 2.0f, (Height - this.statusFont.Height - 2) / 2.0f); int diameter = Math.Min(Width, Height - this.statusFont.Height - 2); int bigRadius = diameter / 2 - DotRadius - (DotCount - 1) * ScaleFactor; // Draw the dots float unitAngle = 360.0f / DotCount; for(int index = 0; index < DotCount; ++index) { int dotIndex = (index + leadingDotIndex) % DotCount; var dotPosition = new PointF( center.X + (float)(bigRadius * Math.Cos(unitAngle * dotIndex * Math.PI / 180.0f)), center.Y + (float)(bigRadius * Math.Sin(unitAngle * dotIndex * Math.PI / 180.0f)) ); int currentDotRadius = DotRadius + index * ScaleFactor; var corner = new PointF( dotPosition.X - currentDotRadius, dotPosition.Y - currentDotRadius ); arguments.Graphics.FillEllipse( this.dotFillBrush, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius ); arguments.Graphics.DrawEllipse( this.dotOutlinePen, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius ); } } finally { arguments.Graphics.SmoothingMode = prevousSmoothingMode; } } /// Draws the status message under the animated dots /// Provides access to the drawing surface and tools private void paintStatusMessage(PaintEventArgs arguments) { if(!string.IsNullOrEmpty(this.statusText)) { SizeF textRectangle = arguments.Graphics.MeasureString( this.statusText, this.statusFont ); var messageArea = new RectangleF( (Width - textRectangle.Width) / 2.0f, Height - this.statusFont.Height - 1.0f, textRectangle.Width, this.statusFont.Height ); // Draw text with a white halo. This is a little bit ugly... { messageArea.Offset(-1.0f, 0.0f); arguments.Graphics.DrawString( this.statusText, this.statusFont, Brushes.White, messageArea ); messageArea.Offset(2.0f, 0.0f); arguments.Graphics.DrawString( this.statusText, this.statusFont, Brushes.White, messageArea ); messageArea.Offset(-1.0f, -1.0f); arguments.Graphics.DrawString( this.statusText, this.statusFont, Brushes.White, messageArea ); messageArea.Offset(0.0f, 2.0f); arguments.Graphics.DrawString( this.statusText, this.statusFont, Brushes.White, messageArea ); messageArea.Offset(0.0f, -1.0f); arguments.Graphics.DrawString( this.statusText, this.statusFont, this.dotFillBrush, messageArea ); } } } /// Called when the animation timer ticks to update the animation state /// Animation timer that has ticked /// Not used private void animationTimerTicked(object sender, EventArgs arguments) { this.leadingDotIndex = (this.leadingDotIndex + 1) % DotCount; // Advance the animation Invalidate(); // Request a redraw at the earliest opportune time } /// Whether the spinner has been started private bool spinnerRunning; /// Index of the currently leading dot private int leadingDotIndex = 0; /// Text that will be displayed under the control as the current status private string statusText; /// Color in which the dots will be filled private Color dotFillColor = Color.RoyalBlue; /// Color that will be used for the dots' outline private Color dotOutlineColor = Color.White; /// Brush used to fill the dots private Brush dotFillBrush; /// Brush used for the dots' outline private Pen dotOutlinePen; /// Font that is used to display the status text private Font statusFont = SystemFonts.SmallCaptionFont; } } // namespace Nuclex.Windows.Forms.Controls