Reputation: 4059
I wanted to develop a label which would show the text in a fade-in reveal type fashion. I expect that I need to handle the painting so that, essentially, a few more pixels are added to the right over each iteration. However, I've run into a snag and can't get any animation working. Here's what I have so far:
class RevealLabel : System.Windows.Forms.Label
{
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
const int GradWidth = 7;
Rectangle r = new Rectangle(e.ClipRectangle.X, e.ClipRectangle.Y,
GradWidth, e.ClipRectangle.Height);
GraphicsPath p = new GraphicsPath();
using (SolidBrush bs = new SolidBrush(this.ForeColor))
{
using (LinearGradientBrush bg = new LinearGradientBrush(new Rectangle(0, 0, GradWidth, e.ClipRectangle.Height),
this.ForeColor,
this.BackColor,
LinearGradientMode.Horizontal))
{
for (int i = 0; i < e.ClipRectangle.Width; i += GradWidth)
{
r.X = i;
p.AddString(this.Text, this.Font.FontFamily, (int)this.Font.Style, this.Font.Size,
r, StringFormat.GenericDefault);
e.Graphics.FillPath(bg, p);
e.Graphics.Flush();
long TickStop = DateTime.Now.AddSeconds(0.2).Ticks;
while (DateTime.Now.Ticks < TickStop)
{
System.Windows.Forms.Application.DoEvents();
}
e.Graphics.FillPath(bs, p);
}
}
}
e.Graphics.Flush();
}
}
Not sure if I'm even on the right track, as what this renders is just a horrible mess. A mess which isn't even gradually displayed to the screen, but rather seems to process everything in the background then updates the screen with just the end result.
So, my question is two-fold:
What is the correct way to render the pixels/rectangle area I will be appending to the right on each iteration?
How to get it to update to the screen on each draw, instead of just a lump upon completion?
(Note: I also tried painting in a background thread, but kept getting ArgumentException, I think because the Graphics object went out of a usable state soon after leaving the paint handler method.)
Upvotes: 1
Views: 1360
Reputation: 4059
After hammering away at this, I finally came around with the solution, here:
public class RevealLabel : System.Windows.Forms.Control
{
private const int GradWidth = 5;
private const int DrawDelay = 20;
private int lcBackgroundPaintThreadId;
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
e.Graphics.Clear(this.BackColor);
if (this.DesignMode)
{
using (SolidBrush bs = new SolidBrush(this.ForeColor))
{
e.Graphics.DrawString(this.Text, this.Font, bs, 0, 0);
}
}
else
{
System.Threading.ThreadPool.QueueUserWorkItem(QueuePaintStep, this.ClientRectangle.X);
}
}
private void QueuePaintStep(object state)
{
lcBackgroundPaintThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
System.Threading.Thread.Sleep(DrawDelay);
if (System.Threading.Thread.CurrentThread.ManagedThreadId == lcBackgroundPaintThreadId)
{
int x = (int)state;
if (x < this.ClientRectangle.Width)
{
Rectangle r = new Rectangle(x, this.ClientRectangle.Y, GradWidth, this.ClientRectangle.Height);
if (System.Threading.Thread.CurrentThread.ManagedThreadId != lcBackgroundPaintThreadId) return;
using (LinearGradientBrush bg = new LinearGradientBrush(new Rectangle(0, 0, GradWidth, r.Height),
this.ForeColor,
this.BackColor,
LinearGradientMode.Horizontal))
{
this.Invoke(AsyncPaintCommon, this, bg, r);
}
System.Threading.Thread.Sleep(DrawDelay);
if (System.Threading.Thread.CurrentThread.ManagedThreadId != lcBackgroundPaintThreadId) return;
using (SolidBrush bs = new SolidBrush(this.ForeColor))
{
this.Invoke(AsyncPaintCommon, this, bs, r);
}
if (System.Threading.Thread.CurrentThread.ManagedThreadId != lcBackgroundPaintThreadId) return;
QueuePaintStep(x + GradWidth);
}
}
}
private delegate void AsyncPaintDelegate(RevealLabel l, Brush b, Rectangle r);
private static Delegate AsyncPaintCommon = new AsyncPaintDelegate((RevealLabel l, Brush b, Rectangle r) =>
{
using (Graphics g = l.CreateGraphics())
{
g.SetClip(r);
g.DrawString(l.Text, l.Font, b, 0, 0);
}
});
}
Welcoming any feedback.
Upvotes: 0
Reputation: 81620
Try using a timer:
public class RevealLabel : Label {
private System.Windows.Forms.Timer revealTimer = new System.Windows.Forms.Timer();
private int paintWidth = 0;
private int paintIncrement = 7;
public RevealLabel() {
this.DoubleBuffered = true;
revealTimer.Interval = 200;
revealTimer.Tick += new EventHandler(revealTimer_Tick);
}
void revealTimer_Tick(object sender, EventArgs e) {
paintWidth += paintIncrement;
if (paintWidth > this.ClientSize.Width) {
revealTimer.Enabled = false;
} else {
this.Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e) {
if (revealTimer.Enabled) {
e.Graphics.Clear(this.BackColor);
Rectangle r = new Rectangle(0, 0, paintWidth, this.ClientSize.Height);
TextRenderer.DrawText(e.Graphics, this.Text, this.Font, r, Color.Black, Color.Empty, TextFormatFlags.Left | TextFormatFlags.VerticalCenter);
} else {
paintWidth = 0;
revealTimer.Start();
}
}
}
I would probably inherit from a Panel
instead of a Label
since you are doing all of the drawing yourself anyway.
If you want the label to always animate, then move the this.Invalidate()
call in the Tick
event outside of the IF block.
Upvotes: 1