Cross-thread operation not valid (Custom .NET Marquee Control)

R

Ratfish

I'm getting a "Cross-thread operation not valid" error when I call
Refresh() on a custom .NET Marquee control. This control used to be
working in prior .NET projects and I'm trying to get it going again.
It was really a great Marquee control in that it could display both
text and graphics and could run in any direction, etc. I've included
the full source to the control and to the demo form I'm trying to test
it with. To help resolve the problem and for anyone else that might
benefit from the control. I just can't figure out why I'm calling the
controls.Refresh() method from a cross thread. If anyone can shed
some light on this, it would be greatly appreciated.

Aaron

Here's the code for the marquee control:

using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Data;
using System.Windows.Forms;
using System.Text;
using System.Drawing.Design;
using System.Timers;

namespace RADMarquee
{
[DefaultProperty("DrawingMode"),
DefaultEvent("DrawingModeChanged")]
public class Marquee : System.Windows.Forms.UserControl
{
#region Privat Members
Graphics _gfx;
int x, y; // coords of top left corner of graphic

float StringWidth = 0;
float StringHeight = 0;
int BoxPaddingTop = 2;

bool bNewBrush = true;
SolidBrush BrushBG;
SolidBrush BrushBG2;
SolidBrush BrushFG;
System.Timers.Timer _tmrMain;
bool bNewText = true;

public enum Direction
{
Up, Down, Right, Left
};

public enum TextRefreshTiming
{
Async, TextOffScreen
};

const string _sMessageDelimiter = "!^#-=_";
const string sBR = "\r\n";

float StringPadding = 2;
#endregion

#region Properties
TextRefreshTiming _refreshTiming = TextRefreshTiming.Async;
[Browsable(true),
ReadOnly(false),
Category("Marquee"),
Description("Not currently working. Async: Display new Text
right after Text is changed. "
+ "TextOffScreen: Wait until the Text scrolls off the screen
before restart.")]
public TextRefreshTiming TextRefreshTimimg
{
get{ return _refreshTiming; }
set
{
_refreshTiming = (TextRefreshTiming)value;
}
}

// Direction
Direction _direction = Direction.Left;
[Browsable(true),
Category("Marquee"),
Description("Marquee Scrolling Direction"),
ReadOnly(false)]
public Direction ScrollingDirection
{
get { return _direction; }
set
{
_direction = value;
}
}

// Refresh Rate
int iRefreshRate = 100;
[Browsable(true),
ReadOnly(false),
Category("Marquee"),
DefaultValue(typeof(int), "100"),
Description("How long to wait before redrawing the Marquee in
milliseconds (set this to a large value to improve perormance)")]
public int RefreshRate
{
get { return iRefreshRate; }
set
{
iRefreshRate = value;
}
}

// Speed
int iSpeed = 8;
[Browsable(true),
ReadOnly(false),
DefaultValue(typeof(int), "8"),
Category("Marquee"),
Description("How far to advance the Marquee during each
refresh")]
public int Speed
{
get { return iSpeed; }
set
{
iSpeed = value;
}
}

public int PauseCycles = 5;

// RegionColor
private Color _bgColor = Color.Black;
[Browsable(true),
ReadOnly(false),
DefaultValue(typeof(Color), "Color.Black"),
Category("Marquee"),
Description("Background Color of Marquee")]
public Color RegionColor
{
get { return _bgColor; }
set
{
_bgColor = value;
bNewBrush = true;
}
}

// BG2Color
private Color _bg2Color = Color.Black;
[Browsable(true),
ReadOnly(false),
DefaultValue(typeof(Color), "Color.Black"),
Category("Marquee"),
Description("Background Color of Marquee")]
public Color BGColor
{
get { return _bg2Color; }
set
{
_bg2Color = value;
bNewBrush = true;
}
}

// ForeColor
Color _foreColor = Color.Red;
[Browsable(true),
ReadOnly(false),
Category("Marquee"),
Description("Marquee Text Color"),
DefaultValue(typeof(Color), "Color.Red")]
public override Color ForeColor
{
get { return _foreColor; }
set
{
_foreColor = value;
bNewBrush = true;
}
}

// Font
private Font _font;
[Browsable(true),
ReadOnly(false),
Category("Marquee"),
Description("Marquee Font Properties ")]
public new Font Font
{
get { return _font; }
set
{
_font = value;
bNewText = true;
}
}

// DisplayText
StringBuilder _sbDisplayCurrent;
[Browsable(true),
Category("Marquee"),
Description("Marquee Display Text"),
ReadOnly(false)]
public string DisplayText
{
get
{
if(!Object.Equals(_sbDisplayCurrent, null))
{
return _sbDisplayCurrent.ToString();
}
else
{
return "";
}
}
set
{
if(Object.Equals(_sbDisplayCurrent, null)) // null
_sbDisplayCurrent
{
_sbDisplayCurrent = new StringBuilder(value);
}

LoadStringCollection(value);
// refresh String on next cycle
if(/*this.TextRefreshTimimg == TextRefreshTiming.Async &&
*/value != "")
{
bNewText = true;
StartDraw(0);
}
}
}

private void LoadStringCollection(string sVal)
{
_sbDisplayCurrent.Remove(0, _sbDisplayCurrent.Length);
_sbDisplayCurrent.Append(sVal);
}

// Enabled
bool bEnabled = true;
[Browsable(true),
ReadOnly(false),
DefaultValue(typeof(bool),"True"),
Category("Marquee"),
Description("Turn the Marquee off or on. Can be used to stop
scrolling")]
public new bool Enabled
{
get { return bEnabled; }
set
{
bEnabled = value;
}
}
#endregion

#region Events
// TextOffScreen
[Browsable(true),
ReadOnly(false),
Category("Marquee"),
Description("Event to raise when text has cleared visible area,
occurs just before TextEnd")]
public event EventHandler TextOffScreen;

// TextEnd
[Browsable(true),
ReadOnly(false),
Category("Marquee"),
Description("Event to raise when Marquee text has cycled once
and is about to start over, occurs just afterTextOffScreen")]
public event EventHandler TextEnd;

private System.ComponentModel.Container components = null;
#endregion

#region Constructor
public Marquee()
{
// this call is required by the Windows.Forms Form Designer.
_sbDisplayCurrent = new StringBuilder();

InitializeComponent();

this.Size = new Size(400,200);
this.Text = "Marquee";

SetStyle(ControlStyles.UserPaint, true); // enable dbl
bufferg
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);

Init();

_tmrMain = new System.Timers.Timer();
_tmrMain.Elapsed += new
ElapsedEventHandler(this.DoDrawEvent);
_tmrMain.Interval = this.RefreshRate;
_tmrMain.Start();
}
#endregion

public void Quit()
{
this.Dispose();
}

public void Stop()
{

}

public void Init()
{
if (Object.Equals(_font, null))
_font = new Font("Arial", 14, FontStyle.Bold);
SetDefaults();
}

public void SetDefaults()
{
this.ForeColor = Color.Red;
this.BGColor = Color.Black;
this.RegionColor = Color.Black;
}

public void LoadBrushes()
{
bool bTmp = bEnabled; // save enabled state
bEnabled = false; // disable
// load colors
BrushBG = new SolidBrush(RegionColor);
BrushBG2 = new SolidBrush(BGColor);
BrushFG = new SolidBrush(ForeColor);
bEnabled = bTmp; //EnabledState.Disabled;
bNewBrush = false;
DrawBackground();
}

protected override void OnPaint(PaintEventArgs e)
{
_gfx = e.Graphics;

if(bNewBrush)
LoadBrushes();

if(bNewText)
BuildStringGraphic(e.Graphics);

_gfx.FillRectangle(BrushBG, 0, 0, Width, Height);

// add support here for using a different brush to draw the
strings/portions of the string
_gfx.DrawString(_sbDisplayCurrent.ToString(), _font, BrushFG,
x, y);
}

protected override void OnPaintBackground(PaintEventArgs
pevent)
{
// don't allow the background to paint
}

protected void BuildStringGraphic(Graphics g)
{
// examine the string for settings mark up
// <setting[\s\w\d\"]+/>
// message characters: [\w\s\d\.\"!-\+=&\*\(\)\$%@]+
// <setting bgcolor="" color="" font="" />
//ParseMessages(); // look for settings and apply here
if (Object.Equals(_sbDisplayCurrent, null) ||
Object.Equals(_font, null) || _sbDisplayCurrent.ToString() == "")
return;

StringFormat sfTrue =
(StringFormat)StringFormat.GenericTypographic.Clone();
sfTrue.Alignment = StringAlignment.Near;
sfTrue.LineAlignment = StringAlignment.Near;

SizeF dims = g.MeasureString(_sbDisplayCurrent.ToString(),
_font);
StringWidth = dims.Width;
StringHeight = dims.Height;

// set initial coords by direction
if(_direction == Direction.Right)
{
x = -(int)(StringPadding + StringWidth); // start at left
y = (int)((Height - StringHeight)/ 2 ) + BoxPaddingTop;
}
else if(_direction == Direction.Left)
{
x = (Width + (int)(StringPadding + StringWidth));
y = (int)((Height - StringHeight) / 2 ) + BoxPaddingTop;
}
else if(_direction == Direction.Up)
{
x = (int)StringPadding;
y = (int)(Height / 2);
}
else if(_direction == Direction.Down)
{
x = (int)StringPadding;
y = (int)-(Height / 2);
}
bNewText = false;
}

protected void DrawBackground()
{
Rectangle _rec = new Rectangle(0, BoxPaddingTop, Width,
Height - BoxPaddingTop * 2);
_gfx.FillRectangle(BrushBG2, _rec); // fill the second
background
}

public void DoDrawEvent(object sender, ElapsedEventArgs e)
{
DoDrawingCycle();
}

public void StartDraw(int iPauseCycles)
{
PauseCycles = iPauseCycles;

// don't do anything if this control is disabled
if(bEnabled == false || DisplayText == "")
return;

DoDrawingCycle();
}

protected void DoDrawingCycle()
{


if(_direction == Direction.Right)
{
this.MoveRight();
}
else if(_direction == Direction.Left)
{
this.MoveLeft();
}
else if(_direction == Direction.Down)
{
this.MoveDown();
}
else if(_direction == Direction.Up)
{
this.MoveUp();
}
}

public void MoveLeft()
{
int i = 0;
bool bTextCleared = false;
if( x < -(StringPadding + StringWidth) ) // restart from
right
{
x = Width + (int)StringPadding;
i = 0;
bTextCleared = false;
OnTextEnd(null);
}
else if (x < -(StringWidth) && !bTextCleared)
{
bTextCleared = true;
OnTextClear(null);
}

if(i < PauseCycles)
++i;
else
x = x - Speed;

// folling call is generating this error:
// Cross-thread operation not valid: Control 'mq' accessed
from a thread other than the thread it was created on.
Refresh();
}

public void MoveRight()
{
int i = 0;
bool bTextCleared = false;
if( x > (Width) ) // restart from left
{
x = -Width;
i = 0;
bTextCleared = false;
OnTextEnd(null);
}
else if (x == Width && !bTextCleared)
{
bTextCleared = true;
OnTextClear(null);
}

if(i < PauseCycles)
++i;
else
x = x + Speed;
Refresh();
}

public void MoveUp()
{
int i = 0;
bool bTextCleared = false;

if( y < -(StringPadding + StringHeight + Height) )
{
y = Height + (int)StringPadding;
x = 0;
i = 0;
bTextCleared = false;
OnTextEnd(null);
}
else if (y == -StringHeight && !bTextCleared)
{
bTextCleared = true;
OnTextClear(null);
}

if(i < PauseCycles)
++i;
else
y = y - Speed;

Refresh();
}

public void MoveDown()
{
int i = 0;
bool bTextCleared = false;
if( y > (Height + StringPadding) ) // restart from left
{
x = 0;
y = 0 - (int)StringHeight ;
i = 0;
bTextCleared = false;
OnTextEnd(null);
}
else if (y == Height && !bTextCleared)
{
bTextCleared = true;
OnTextClear(null); // no EventArgs
}

if(i < PauseCycles)
++i;
else
y = y + Speed;

Refresh();
}


#region Event Handling
// invoke delegates when the Marquee text has cleared the
display area
protected virtual void OnTextClear(MarqueeEventArgs e)
{
if (TextOffScreen != null && DisplayText != "")
{
// invokes the delegates.
TextOffScreen(this, e);
}
}

// invoke delegates when the marquee text has reached the
absolute end of its cycle
protected virtual void OnTextEnd(MarqueeEventArgs e)
{
if (TextEnd != null && DisplayText != "")
{
TextEnd(this, e);
}
}

protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

private void Marquee_Resize(object sender, System.EventArgs e)
{
this.Invalidate();
}
#endregion

#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.Resources.ResourceManager resources = new
System.Resources.ResourceManager(typeof(Marquee));
//
// Marquee
//
this.BackColor = System.Drawing.Color.WhiteSmoke;
this.BackgroundImage = ((System.Drawing.Image)
(resources.GetObject("$this.BackgroundImage")));
this.Name = "Marquee";
this.Resize += new System.EventHandler(this.Marquee_Resize);

}
#endregion


}

public class MarqueeEventArgs : EventArgs
{
int iFlags = 0;
public MarqueeEventArgs()
{
}
}
}




Here's the code for the demo form:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace RadMarqueeDemo
{
public partial class Form1 : Form
{
private RADMarquee.Marquee mq;

private void MyInitializeComponent()
{
mq = new RADMarquee.Marquee();

this.mq.ForeColor = System.Drawing.Color.Red;
this.mq.BGColor = Color.Black;

this.mq.Location = new System.Drawing.Point(10, 10);
this.mq.Name = "mq";
this.mq.ScrollingDirection =
RADMarquee.Marquee.Direction.Left;
this.mq.Size = new System.Drawing.Size(300, 50);
mq.RefreshRate = 70;
this.mq.Speed = 7;
this.mq.TabIndex = 0;

this.Controls.Add(this.mq);
mq.Enabled = true;

mq.DisplayText = "Hello World!";
}

public Form1()
{
MyInitializeComponent();
InitializeComponent();
}

private void Form1_Load_1(object sender, EventArgs e)
{

}

private void timer1_Tick(object sender, EventArgs e)
{
this.mq.Enabled = true;
}
}
}
 
P

Peter Duniho

Ratfish said:
[...] I just can't figure out why I'm calling the
controls.Refresh() method from a cross thread. If anyone can shed
some light on this, it would be greatly appreciated.

That is WAY too much code to post for a question like that. You really
should have trimmed it down to the bare minimum required to reproduce
the problem.

That said, I did a text search on your post to find that in there
somewhere you are using the System.Timers.Timer class. The notification
for that timer class executes on a different thread than the one you use
to create it. So if you're calling Refresh() from that thread, you
would see the problem you describe.

You really should not be calling Refresh() anyway. Your UI should
automatically invalidate itself based on state changes. The state
changes themselves will need to be effected on the thread that owns the
UI objects (generally your main program thread), which you can
accomplish using the Control.Invoke() method.

A Google Groups search on "Control.Invoke" and/or "cross-thread
exception" in this newsgroup will reveal a wealth of past discussions on
the topic.

Pete
 
R

Ratfish

Sorry about the long post. Thanks for the tip. This snippet of code
got it working again:

if (this.InvokeRequired)
{
this.BeginInvoke(new RefreshDelegate(Refresh));
}
else
Refresh();


Ratfish said:
[...] I just can't figure out why I'm calling the
controls.Refresh() method from a cross thread.  If anyone can shed
some light on this, it would be greatly appreciated.

That is WAY too much code to post for a question like that.  You really
should have trimmed it down to the bare minimum required to reproduce
the problem.

That said, I did a text search on your post to find that in there
somewhere you are using the System.Timers.Timer class.  The notification
for that timer class executes on a different thread than the one you use
to create it.  So if you're calling Refresh() from that thread, you
would see the problem you describe.

You really should not be calling Refresh() anyway.  Your UI should
automatically invalidate itself based on state changes.  The state
changes themselves will need to be effected on the thread that owns the
UI objects (generally your main program thread), which you can
accomplish using the Control.Invoke() method.

A Google Groups search on "Control.Invoke" and/or "cross-thread
exception" in this newsgroup will reveal a wealth of past discussions on
the topic.

Pete
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top