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