I am trying to make a rss news ticker which will display the text,the text need move from left to right
I made the the code and text is moving from left to right but after a specific time its not showing the full text,i will be adding more news from the admin pannel,each time i add the news the text is not showing after the first scroll
the below screenshot is after a specific amount of time,only a part of news is displaying
Code used
int x = -800,y=1;
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
private void timer1_Tick(object sender, System.EventArgs e)
{
label1.SetBounds(x, y, 1, 1);
x++;
if(x>=800)
{
x = 4;
}
}
Code for reading xml
private void StartRssThread()
{
List<RssChannel> channels = new List<RssChannel>();
StringBuilder mergedFeed = new StringBuilder();
int mh = 0;
int ms = 0;
if (mh < 7)
{
RssFeed DaFeed = RssFeed.Read("http://shjc.ae/rss/fileName.xml");
RssChannel DaChannel = (RssChannel)DaFeed.Channels[0];
channels.Add(DaChannel);
mergedFeed.AppendFormat(" {0}: ", DaChannel.Title);
foreach (RssItem sTrm in DaChannel.Items)
{
if (ms < 10)
{
mergedFeed.AppendFormat(" {0} |", sTrm.Title);
ms++;
mh++;
}
}
}
string dafeed = mergedFeed.ToString();
mergedFeed = null;
textBox1.Invoke(new UpdateUiCallback(this.UpdateUi), new string[] { dafeed });
}
Windows Forms Marquee Label - Horizontal
I've already posted an example of how to create a Marquee Label to animate text from top to bottom, by using a Timer and overriding OnPaint method of a custom draw control in this post: Windows Forms Top to Bottom Marquee Label.
In the following code, I've changed that example to animates the text horizontally from right to left or left to right. It's enough to set RightToLeft property of the control to change the direction. Also don't forget to set AutoSize to false.
Example - Right to Left and Left to Right Marquee Label in Windows Forms
using System;
using System.Drawing;
using System.Windows.Forms;
public class MarqueeLabel : Label
{
Timer timer;
public MarqueeLabel()
{
DoubleBuffered = true;
timer = new Timer() { Interval = 100 };
timer.Enabled = true;
timer.Tick += Timer_Tick;
}
int? left;
int textWidth = 0;
private void Timer_Tick(object sender, EventArgs e)
{
if (RightToLeft == RightToLeft.Yes)
{
left += 3;
if (left > Width)
left = -textWidth;
}
else
{
left -= 3;
if (left < -textWidth)
left = Width;
}
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.Clear(BackColor);
var s = TextRenderer.MeasureText(Text, Font, new Size(0, 0),
TextFormatFlags.TextBoxControl | TextFormatFlags.SingleLine);
textWidth = s.Width;
if (!left.HasValue) left = Width;
var format = TextFormatFlags.TextBoxControl | TextFormatFlags.SingleLine |
TextFormatFlags.VerticalCenter;
if (RightToLeft == RightToLeft.Yes)
{
format |= TextFormatFlags.RightToLeft;
if (!left.HasValue) left = -textWidth;
}
TextRenderer.DrawText(e.Graphics, Text, Font,
new Rectangle(left.Value, 0, textWidth, Height),
ForeColor, BackColor, format);
}
protected override void Dispose(bool disposing)
{
if (disposing)
timer.Dispose();
base.Dispose(disposing);
}
}
You are using hard coded values for the start and max value of x. From your question I think the text in the label has a dynamic length (correct?). If the text is of dynamic length the value of x should also be dynamic.
Also, x starts at -800. Then slowly grows to 800 and then it gets set to 4.
This seems strange to me, if the first run started at -800, the second run may also need to start at -800.
Hope this somewhat helped you. If not, please provide more details (like why you chose -800, 800 and 4).
Related
Windows Forms ZedGraph control in WPF application. The data points are auto-generated and attached to the chart every N seconds. When new data point is added to the chart I shift (pan) chart one point to the left, so there is always no more than last 50 points visible in the viewport. Overall, it works pretty good, except for two things.
Issues
If user tries to zoom in or out, the viewport stops following the last data point and chart goes outside of the screen, so panning stops working
I would like to pan or shift chart using mouse event, without scrolling, but when I press right mouse button and try to move it to the left, it tries to zoom chart instead of panning
protected void CreateChart(ZedGraphControl control)
{
_rand = new Random();
var curve = control.GraphPane.AddJapaneseCandleStick("Demo", new StockPointList());
curve.Stick.IsAutoSize = true;
curve.Stick.Color = Color.Blue;
control.AutoScroll = true; // Always shift to the last data point when new data comes in
control.IsEnableHPan = true; // I assume this should allow me to move chart to the left using mouse
control.IsEnableVPan = true;
control.IsEnableHZoom = true;
control.IsEnableVZoom = true;
control.IsShowPointValues = true;
control.IsShowHScrollBar = false;
control.IsShowVScrollBar = false;
control.IsAutoScrollRange = true; // Always shift to the last data point when new data comes in
control.IsZoomOnMouseCenter = false;
control.GraphPane.XAxis.Type = AxisType.DateAsOrdinal;
control.AxisChange();
control.Invalidate();
var aTimer = new Timer();
aTimer.Elapsed += new ElapsedEventHandler(OnTime);
aTimer.Interval = 100;
aTimer.Enabled = true;
}
protected XDate _xDate = new XDate(2006, 2, 1);
protected double _open = 50.0;
protected Random _rand = new Random();
// Auto generate data points
protected void OnTime(object source, ElapsedEventArgs e)
{
var control = FormCharts;
var x = _xDate.XLDate;
var close = _open + _rand.NextDouble() * 10.0 - 5.0;
var hi = Math.Max(_open, close) + _rand.NextDouble() * 5.0;
var low = Math.Min(_open, close) - _rand.NextDouble() * 5.0;
var pt = new StockPt(x, hi, low, _open, close, 100000);
_open = close;
_xDate.AddDays(1.0);
if (XDate.XLDateToDayOfWeek(_xDate.XLDate) == 6)
{
_xDate.AddDays(2.0);
}
(control.GraphPane.CurveList[0].Points as StockPointList).Add(pt);
control.GraphPane.XAxis.Scale.Min = control.GraphPane.XAxis.Scale.Max - 50; // Hide all points except last 50, after mouse zooming this line stops working
//control.GraphPane.XAxis.Scale.Max = control.GraphPane.XAxis.Scale.Max + 1;
control.AxisChange();
control.Invalidate();
}
Kind of solved it.
First issue with broken auto-scroll after zooming
It happens because zooming sets these parameters to FALSE.
area.XAxis.Scale.MinAuto = false;
area.XAxis.Scale.MaxAuto = false;
To fix it, either set it back to TRUE every time new data point comes. Another way to fix it, is to keep them always as FALSE and move chart manually
protected void MoveChart(GraphPane pane, int pointsToMove, int pointsToShow)
{
pane.XAxis.Scale.Max = pane.XAxis.Scale.Max - pointsToMove;
pane.XAxis.Scale.Min = pane.XAxis.Scale.Max - Math.Abs(pointsToShow);
}
...
// Example : shift one point to the left and show only 50 last points
MoveChart(control.MasterPane.PaneList["Quotes"], -1, 50);
Second issue, implementing custom panning without scrollbar using mouse events.
protected int _mouseX = -1;
protected int _mouseY = -1;
...
control.MouseUpEvent += OnMouseUp;
control.MouseDownEvent += OnMouseDown;
control.MouseMoveEvent += OnMouseMove;
...
// Example : remember X and Y on mouse down and move chart until mouse up event
protected bool OnMouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
_mouseX = -1; // unset X on mouse up
_mouseY = -1;
return true;
}
protected bool OnMouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
_mouseX = e.X; // remember last X on mouse down
_mouseY = e.Y;
return true;
}
protected bool OnMouseMove(ZedGraphControl sender, System.Windows.Forms.MouseEventArgs e)
{
if (_mouseX >= 0) // if X was saved after mouse down
{
foreach (var pane in sender.MasterPane.PaneList) // move synced chart panels
{
MoveChart(pane, _mouseX > e.X ? -1 : 1, 50); // if mouse move is increasing X, move chart to the right, and vice versa
}
_mouseX = e.X; // update X to new position
_mouseY = e.Y;
}
return true;
}
I have records in my .NET WinForms app that I lay out with enhanced TextBox controls on panels when the records are editable, but I set the TextBoxes to ReadOnly when the records are not editable. Clicking the save button on an editable record saves the text to the database, and then it is displayed as an un-editable record (until the edit button is clicked). Please see the following screen grab:
As you can hopefully see, the first record is not editable, but the second one is. The problem I have is that I would like the TextBox to grow in Height if the text is too much to fit. It seems that the TextBox is doing the WordWrap, but it either only shows one line of the text or only the first two. Something is always cut off at the bottom.
I have looked at several other posts on this site, including, especially, Expandable WinForms TextBox.
Here is some sample code for the panel:
AutoSize = true;
AutoSizeMode = AutoSizeMode.GrowAndShrink;
...
Field1 = new ExpandoField { Multiline = true, WordWrap = true };
Field1.Location = new System.Drawing.Point(42, 3);
if (CanEdit)
{
Field1.BackColor = System.Drawing.Color.White;
Field1.TabIndex = 20;
}
else
{
((ExpandoField) Field1).ReadOnly = true;
Field1.ForeColor = System.Drawing.Color.FromArgb(0, 50, 0);
Field1.BackColor = System.Drawing.Color.Snow;
Field1.TabIndex = 0;
Field1.TabStop = false;
}
Field1.Text = Text1;
Field1.Dock = DockStyle.None;
Field1.Size = new System.Drawing.Size(538 - 25, 34);
Field1.MinimumSize = Field1.Size;
Field1.AutoSize = true;
Controls.Add(Field1);
As you can see, I have AutoSize set to true for the panel. The code for Field2 is similar to Field1.
ExpandoField is based on sample code I saw from a response by dstran in Expandable WinForms TextBox. It seemed to be the most complete implementation of the suggestion marked as the answer to that post. Here's the code:
class ExpandoField : TextBox
{
private double m_growIndex = 0.0;
private Timer m_timer;
public ExpandoField()
{
AutoSize = false;
this.Height = 20;
// Without the timer, I got a lot of AccessViolationException in the System.Windows.Forms.dll.
m_timer = new Timer();
m_timer.Interval = 1;
m_timer.Enabled = false;
m_timer.Tick += new EventHandler(m_timer_Tick);
this.KeyDown += new KeyEventHandler(ExpandoField_KeyDown);
}
void ExpandoField_KeyDown(object sender, KeyEventArgs e)
{
if (e.Modifiers == Keys.Control && e.KeyCode == Keys.A)
this.SelectAll();
}
void m_timer_Tick(object sender, EventArgs e)
{
var sz = new System.Drawing.Size(Width, Int32.MaxValue);
sz = TextRenderer.MeasureText(Text, Font, sz, TextFormatFlags.TextBoxControl);
m_growIndex = (double)(sz.Width / (double)Width);
if (m_growIndex > 0)
Multiline = true;
else
Multiline = false;
int tempHeight = (int) (20 * m_growIndex);
if (tempHeight <= 20)
Height = 20;
else
Height = tempHeight;
m_timer.Enabled = false;
}
public override sealed bool AutoSize
{
get { return base.AutoSize; }
set { base.AutoSize = value; }
}
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
m_timer.Enabled = true;
}
protected override void OnFontChanged(EventArgs e)
{
base.OnFontChanged(e);
m_timer.Enabled = true;
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
m_timer.Enabled = true;
}
}
This is obviously not quite working. I have the panel set to AutoSize, but it is not growing to accomodate the second TextBox. Also, I need to somehow push the second TextBox down when the first one grows. Is there some good way for the panel to know when ExpandoField gets an OnSizeChanged event? It seems like the growth of that panel would then need to cause the remainder of the list of panels to be redrawn in lower locations. I'm not sure how to get this cascade effect to work right...
I also think the use of the timer seems like an inefficient kluge...
I'm still learning WinForms. Is there some well-designed way I can get the behavior that I want? Is there some event I can catch when the WordWrap takes place (or when the text exceeds the size of the TextBox)? That would allow me to resize the TextBox. And how does the TextBox let the panel know that it has changed? Does it need to call the OnSizeChanged handler for it's parent panel? Does the panel need to call the OnSizeChanged handler for it's parent list?
Any suggestions?
I believe I have the answer, after 3 or 4 failed attempts...
class ExpandoField : TextBox
{
private bool UpdateInProgress = false;
private static System.Text.RegularExpressions.Regex rgx = new System.Text.RegularExpressions.Regex(#"\r\n");
public delegate void CallbackFn();
CallbackFn VSizeChangedCallback;
public ExpandoField(CallbackFn VSizeChanged)
{
AutoSize = false;
VSizeChangedCallback = VSizeChanged;
this.KeyDown += new KeyEventHandler(ExpandoField_KeyDown);
}
public void ExpandoField_KeyDown(object sender, KeyEventArgs e)
{
if (e.Modifiers == Keys.Control && e.KeyCode == Keys.A)
this.SelectAll();
}
public void UpdateSize()
{
if (UpdateInProgress == false && Text.Length > 0)
{
UpdateInProgress = true;
int numLines = 0;
System.Drawing.Size baseSize = new System.Drawing.Size(Width, Int32.MaxValue);
System.Drawing.Size lineSize = baseSize; // compiler thinks we need something here...
// replace CR/LF with single character (paragraph mark 'ΒΆ')
string tmpText = rgx.Replace(Text, "\u00B6");
// split text at paragraph marks
string[] parts = tmpText.Split(new char[1] { '\u00B6' });
numLines = parts.Count();
foreach (string part in parts)
{
// if the width of this line is greater than the width of the text box, add needed lines
lineSize = TextRenderer.MeasureText(part, Font, baseSize, TextFormatFlags.TextBoxControl);
numLines += (int) Math.Floor(((double) lineSize.Width / (double) Width));
}
if (numLines > 1)
Multiline = true;
else
Multiline = false;
int tempHeight = Margin.Top + (lineSize.Height * numLines) + Margin.Bottom;
if (tempHeight > Height || // need to grow...
Height - tempHeight > lineSize.Height) // need to shrink...
{
Height = tempHeight;
VSizeChangedCallback();
}
UpdateInProgress = false;
}
}
public override sealed bool AutoSize
{
get { return base.AutoSize; }
set { base.AutoSize = value; }
}
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
UpdateSize();
}
protected override void OnFontChanged(EventArgs e)
{
base.OnFontChanged(e);
UpdateSize();
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
UpdateSize();
}
}
Note that on the constructor this subclass of TextBox now accepts a delegate callback to let the parent class know that the TextBox has changed its size. (I suppose I should have handled the possibility of a null value here...)
Thankfully, this solution no longer required a timer.
I have tested this code pretty well, and I have watched it both grow & shrink. It respects MaximumSize, and it even handles the presence of carriage-return/line-feed pairs. (This code assumes Windows; it should be trivial to modify it for Linux, etc.) Feel free to suggest improvements.
I want to make a moving label seem nicer and smoother than just reappearing the whole thing to the left after it has all gone out of panel width .For example label 'Hello' , as soon as 'lo' goes out of bounds in the right I want it to reappear on the left. Is there any possible solution to this ?
Here's the code I have for the label now .
private void timer2_Tick(object sender, EventArgs e)
{
label5.Location = new Point(label5.Location.X + 3, label5.Location.Y);
if (label5.Location.X > this.Width)
{
label5.Location = new Point(0 - label5.Width, label5.Location.Y);
}
}
Try this, using a Label (here, named lblMarquee and a System.Windows.Forms.Timer).
The scrolling time is regulated by both the Timer.Interval and a float Field (marqueeStep).
The Timer.Tick event just calls lblMarquee.Invalidate(), causing the Label control to repaint itself.
When the scrolling text, in relation to its current position, goes beyond the limits of the Label.ClientRectangle, the section of the text which is not visible anymore is painted at start of the Label.ClientArea:
System.Windows.Forms.Timer marqueeTimer = new System.Windows.Forms.Timer();
string marqueeText = string.Empty;
float marqueePosition = 0f;
float marqueeStep = 4f;
private void form1_Load(object sender, EventArgs e)
{
marqueeText = lblMarquee.Text;
lblMarquee.Text = string.Empty;
marqueeTimer.Tick += (s, ev) => { this.lblMarquee.Invalidate(); };
marqueeTimer.Interval = 100;
marqueeTimer.Start();
}
private void lblMarquee_Paint(object sender, PaintEventArgs e)
{
var marquee = sender as Label;
SizeF stringSize = e.Graphics.MeasureString(marqueeText, marquee.Font, -1, marqueeFormat);
PointF stringLocation = new PointF(marqueePosition, (marquee.Height - stringSize.Height) / 2);
stringLength = marquee.ClientRectangle.Width - stringLocation.X;
e.Graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
e.Graphics.DrawString(marqueeText, marquee.Font, Brushes.Black, stringLocation, marqueeFormat);
if (marqueePosition >= marquee.ClientRectangle.Width) marqueePosition = 0f;
if (stringSize.Width + stringLocation.X > marquee.ClientRectangle.Width) {
PointF partialStringPos = new PointF(-stringLength, (marquee.Height - stringSize.Height) / 2);
e.Graphics.DrawString(marqueeText, marquee.Font, Brushes.Black, partialStringPos, marqueeFormat);
}
marqueePosition += marqueeStep;
}
A couple of other implementations you might find useful:
How to follow the end of a text in a TextBox with no NoWrap
How to draw a string on two not adjacent areas
You need to have two label controls to do this, but it's not really that difficult. First, create a backup label and set it's properties to look like label5:
// A backup label for our scrolling label5
private Label label5_backup;
private void Form1_Load(object sender, EventArgs e)
{
label5.Text = "This is a scrolling label!";
// Set label5_backup to look like label5
label5_backup = new Label
{
Size = label5.Size,
Text = label5.Text,
Top = label5.Top,
Visible = false
};
Controls.Add(label5_backup);
timer2.Interval = 1;
timer2.Start();
}
Then, in the Tick event, as soon as our label5 starts to leave the client rectangle, set our backup label to the proper distance from the left of the form so that it starts to appear on the other side. And as soon as label5 is completely off the form, set it's location to match the backup label and then hide the backup label again.
Note that you can just set the Left property instead of creating a new Location point each time, which simplifies the code a little:
private void timer2_Tick(object sender, EventArgs e)
{
label5.Left++;
// If label5 starts to go off the right, show our backup on the left side of the form
if (label5.Right > ClientRectangle.Width)
{
label5_backup.Left = label5.Right - ClientRectangle.Width - label5.Width;
label5_backup.Visible = true;
}
// If label5 is all the way off the form now, set it's location to match the backup
if (label5.Left > ClientRectangle.Width)
{
label5.Location = label5_backup.Location;
label5_backup.Visible = false;
}
}
Also, if you want to make the scrolling smoother, only increment the Left by 1 each time and reduce the timer2.Interval to a third of what it was before (unless it's already at 1).
I'm building custom user control that will be used to display tiles map, as base class I've chosen ScrollableControl, because I want to have scrollbars in my control.
I've successfully created paint logic that is responsible for painting only needed elements.
Now I'm trying to add static text that will be always visible in same place (in my case white box with red text in top left corner):
This isn't clearly visible on above gif, but that white box blinks and jumps a bit when I scroll using mouse or scrollbars.
My question is how should I change my code to have scrollable content and fixed position content on top of that scrollable content?
Is ScrollableControl good choice as base class?
Below is my code:
class TestControl : ScrollableControl
{
private int _tileWidth = 40;
private int _tileHeight = 40;
private int _tilesX = 20;
private int _tilesY = 20;
public TestControl()
{
SetStyle(ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.Opaque, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
UpdateStyles();
ResizeRedraw = true;
AutoScrollMinSize = new Size(_tilesX * _tileWidth, _tilesY * _tileHeight);
Scroll += (sender, args) => { Invalidate(); };
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.FillRectangle(new SolidBrush(BackColor), ClientRectangle);
e.Graphics.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
var offsetX = (AutoScrollPosition.X * -1) / _tileWidth;
var offsetY = (AutoScrollPosition.Y * -1) / _tileHeight;
var visibleX = Width / _tileWidth + 2;
var visibleY = Height / _tileHeight + 2;
var x = Math.Min(visibleX + offsetX, _tilesX);
var y = Math.Min(visibleY + offsetY, _tilesY);
for (var i = offsetX; i < x; i++)
{
for (var j = offsetY; j < y; j++)
{
e.Graphics.FillRectangle(Brushes.Beige, new Rectangle(i*_tileWidth, j*_tileHeight, _tileWidth, _tileHeight));
e.Graphics.DrawString(string.Format("{0}:{1}", i, j), Font, Brushes.Black, new Rectangle(i * _tileWidth, j * _tileHeight, _tileWidth, _tileHeight));
}
}
using (var p = new Pen(Color.Black))
{
for (var i = offsetX + 1; i < x; i++)
{
e.Graphics.DrawLine(p, i*_tileWidth, 0, i*_tileWidth, y*_tileHeight);
}
for (var i = offsetY + 1; i < y; i++)
{
e.Graphics.DrawLine(p, 0, i*_tileHeight, x*_tileWidth, i*_tileHeight);
}
}
e.Graphics.FillRectangle(Brushes.White, AutoScrollPosition.X * -1, AutoScrollPosition.Y * -1, 35, 14);
e.Graphics.DrawString("TEST", DefaultFont, new SolidBrush(Color.Red), AutoScrollPosition.X * -1, AutoScrollPosition.Y * -1);
}
}
EDIT:
I've searched a bit and found UserControl that has similar functionality - https://www.codeproject.com/Articles/16009/A-Much-Easier-to-Use-ListView and after reading a bit more on control's author blog http://objectlistview.sourceforge.net/cs/blog1.html#blog-overlays I found out that he is using Transparent Form that is positioned on top of control.
I really would like to avoid that, but still have overlay on top of my control.
You are doing battle with a Windows system option named "Show window content while dragging". Always turned on by default, this web page shows how to turn it off.
Solves the problem, but it is not something you can rely on since it affects all scrollable window in all apps. Demanding that the user turns it off for you is unrealistic, users like this option so they'll just ignore you. That they did not provide an option to turn it off for a specific window was a pretty major oversight. It is an okay solution in a kiosk app.
Briefly, the way the option works is that Windows itself scrolls the window content with the ScrollWindowEx() winapi function. Using a bitblt of the window content to move pixels and only generating a paint request for the part of the window that was revealed by the scroll. Usually only a few lines of pixels, so completes very fast. Problem is, that bitblt moves your fixed pixels as well. The repaint moves them back. Pretty noticeable, the human eye is very sensitive to motion like that, helped avoid being lion lunch for the past million years.
You'll have to take the sting out of ScrollWindowsEx(), preventing it from moving pixels even though you can't stop it from being called. That takes a heavy sledgehammer, LockWindowUpdate(). You'll find code in this post.
using System.Runtime.InteropServices;
...
protected override void OnScroll(ScrollEventArgs e) {
if (e.Type == ScrollEventType.First) {
LockWindowUpdate(this.Handle);
}
else {
LockWindowUpdate(IntPtr.Zero);
this.Update();
if (e.Type != ScrollEventType.Last) LockWindowUpdate(this.Handle);
}
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool LockWindowUpdate(IntPtr hWnd);
Not that pretty, using a separate Label control ought to start sounding attractive.
can you just add a label to that control(on top), in other words - cant you use it as panel?
I have a control (System.Windows.Forms.ScrollableControl) which can potentially be very large. It has custom OnPaint logic. For that reason, I am using the workaround described here.
public class CustomControl : ScrollableControl
{
public CustomControl()
{
this.AutoScrollMinSize = new Size(100000, 500);
this.DoubleBuffered = true;
}
protected override void OnScroll(ScrollEventArgs se)
{
base.OnScroll(se);
this.Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var graphics = e.Graphics;
graphics.Clear(this.BackColor);
...
}
}
The painting code mainly draws "normal" things that move when you scroll. The origin of each shape that is drawn is offsetted by this.AutoScrollPosition.
graphics.DrawRectangle(pen, 100 + this.AutoScrollPosition.X, ...);
However, the control also contains "static" elements, which are always drawn at the same position relative to the parent control. For that, I just don't use AutoScrollPosition and draw the shapes directly:
graphics.DrawRectangle(pen, 100, ...);
When the user scrolls, Windows translates the entire visible area in the direction opposite to the scrolling. Usually this makes sense, because then the scrolling seems smooth and responsive (and only the new part has to be redrawn), however the static parts are also affected by this translation (hence the this.Invalidate() in OnScroll). Until the next OnPaint call has successfully redrawn the surface, the static parts are slightly off. This causes a very noticable "shaking" effect when scrolling.
Is there a way I can create a scrollable custom control that does not have this problem with static parts?
You could do this by taking full control of scrolling. At the moment, you're just hooking in to the event to do your logic. I've faced issues with scrolling before, and the only way I've ever managed to get everything to work smoothly is by actually handling the Windows messages by overriding WndProc. For instance, I have this code to synchronize scrolling between several ListBoxes:
protected override void WndProc(ref Message m) {
base.WndProc(ref m);
// 0x115 and 0x20a both tell the control to scroll. If either one comes
// through, you can handle the scrolling before any repaints take place
if (m.Msg == 0x115 || m.Msg == 0x20a)
{
//Do you scroll processing
}
}
Using WndProc will get you the scroll messages before anything gets repainted at all, so you can appropriately handle the static objects. I'd use this to suspend scrolling until an OnPaint occurs. It won't look as smooth, but you won't have issues with the static objects moving.
Since I really needed this, I ended up writing a Control specifically for the case when you have static graphics on a scrollable surface (whose size can be greater than 65535).
It is a regular Control with two ScrollBar controls on it, and a user-assignable Control as its Content. When the user scrolls, the container sets its Content's AutoScrollOffset accordingly. Therefore, it is possible to use controls which use the AutoScrollOffset method for drawing without changing anything. The Content's actual size is exactly the visible part of it at all times. It allows horizontal scrolling by holding down the shift key.
Usage:
var container = new ManuallyScrollableContainer();
var content = new ExampleContent();
container.Content = content;
container.TotalContentWidth = 150000;
container.TotalContentHeight = 5000;
container.Dock = DockStyle.Fill;
this.Controls.Add(container); // e.g. add to Form
Code:
It became a bit lengthy, but I could avoid ugly hacks. Should work with mono. I think it turned out pretty sane.
public class ManuallyScrollableContainer : Control
{
public ManuallyScrollableContainer()
{
InitializeControls();
}
private class UpdatingHScrollBar : HScrollBar
{
protected override void OnValueChanged(EventArgs e)
{
base.OnValueChanged(e);
// setting the scroll position programmatically shall raise Scroll
this.OnScroll(new ScrollEventArgs(ScrollEventType.EndScroll, this.Value));
}
}
private class UpdatingVScrollBar : VScrollBar
{
protected override void OnValueChanged(EventArgs e)
{
base.OnValueChanged(e);
// setting the scroll position programmatically shall raise Scroll
this.OnScroll(new ScrollEventArgs(ScrollEventType.EndScroll, this.Value));
}
}
private ScrollBar shScrollBar;
private ScrollBar svScrollBar;
public ScrollBar HScrollBar
{
get { return this.shScrollBar; }
}
public ScrollBar VScrollBar
{
get { return this.svScrollBar; }
}
private void InitializeControls()
{
this.Width = 300;
this.Height = 300;
this.shScrollBar = new UpdatingHScrollBar();
this.shScrollBar.Top = this.Height - this.shScrollBar.Height;
this.shScrollBar.Left = 0;
this.shScrollBar.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
this.svScrollBar = new UpdatingVScrollBar();
this.svScrollBar.Top = 0;
this.svScrollBar.Left = this.Width - this.svScrollBar.Width;
this.svScrollBar.Anchor = AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom;
this.shScrollBar.Width = this.Width - this.svScrollBar.Width;
this.svScrollBar.Height = this.Height - this.shScrollBar.Height;
this.Controls.Add(this.shScrollBar);
this.Controls.Add(this.svScrollBar);
this.shScrollBar.Scroll += this.HandleScrollBarScroll;
this.svScrollBar.Scroll += this.HandleScrollBarScroll;
}
private Control _content;
/// <summary>
/// Specifies the control that should be displayed in this container.
/// </summary>
public Control Content
{
get { return this._content; }
set
{
if (_content != value)
{
RemoveContent();
this._content = value;
AddContent();
}
}
}
private void AddContent()
{
if (this.Content != null)
{
this.Content.Left = 0;
this.Content.Top = 0;
this.Content.Width = this.Width - this.svScrollBar.Width;
this.Content.Height = this.Height - this.shScrollBar.Height;
this.Content.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
this.Controls.Add(this.Content);
CalculateMinMax();
}
}
private void RemoveContent()
{
if (this.Content != null)
{
this.Controls.Remove(this.Content);
}
}
protected override void OnParentChanged(EventArgs e)
{
// mouse wheel events only arrive at the parent control
if (this.Parent != null)
{
this.Parent.MouseWheel -= this.HandleMouseWheel;
}
base.OnParentChanged(e);
if (this.Parent != null)
{
this.Parent.MouseWheel += this.HandleMouseWheel;
}
}
private void HandleMouseWheel(object sender, MouseEventArgs e)
{
this.HandleMouseWheel(e);
}
/// <summary>
/// Specifies how the control reacts to mouse wheel events.
/// Can be overridden to adjust the scroll speed with the mouse wheel.
/// </summary>
protected virtual void HandleMouseWheel(MouseEventArgs e)
{
// The scroll difference is calculated so that with the default system setting
// of 3 lines per scroll incremenet,
// one scroll will offset the scroll bar value by LargeChange / 4
// i.e. a quarter of the thumb size
ScrollBar scrollBar;
if ((Control.ModifierKeys & Keys.Shift) != 0)
{
scrollBar = this.HScrollBar;
}
else
{
scrollBar = this.VScrollBar;
}
var minimum = 0;
var maximum = scrollBar.Maximum - scrollBar.LargeChange;
if (maximum <= 0)
{
// happens when the entire area is visible
return;
}
var value = scrollBar.Value - (int)(e.Delta * scrollBar.LargeChange / (120.0 * 12.0 / SystemInformation.MouseWheelScrollLines));
scrollBar.Value = Math.Min(Math.Max(value, minimum), maximum);
}
public event ScrollEventHandler Scroll;
protected virtual void OnScroll(ScrollEventArgs e)
{
var handler = this.Scroll;
if (handler != null)
{
handler(this, e);
}
}
/// <summary>
/// Event handler for the Scroll event of either scroll bar.
/// </summary>
private void HandleScrollBarScroll(object sender, ScrollEventArgs e)
{
OnScroll(e);
if (this.Content != null)
{
this.Content.AutoScrollOffset = new System.Drawing.Point(-this.HScrollBar.Value, -this.VScrollBar.Value);
this.Content.Invalidate();
}
}
private int _totalContentWidth;
public int TotalContentWidth
{
get { return _totalContentWidth; }
set
{
if (_totalContentWidth != value)
{
_totalContentWidth = value;
CalculateMinMax();
}
}
}
private int _totalContentHeight;
public int TotalContentHeight
{
get { return _totalContentHeight; }
set
{
if (_totalContentHeight != value)
{
_totalContentHeight = value;
CalculateMinMax();
}
}
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
CalculateMinMax();
}
private void CalculateMinMax()
{
if (this.Content != null)
{
// Reduced formula according to
// http://msdn.microsoft.com/en-us/library/system.windows.forms.scrollbar.maximum.aspx
// Note: The original formula is bogus.
// According to the article, LargeChange has to be known in order to calculate Maximum,
// however, that is not always possible because LargeChange cannot exceed Maximum.
// If (LargeChange) == (1 * visible part of control), the formula can be reduced to:
if (this.TotalContentWidth > this.Content.Width)
{
this.shScrollBar.Enabled = true;
this.shScrollBar.Maximum = this.TotalContentWidth;
}
else
{
this.shScrollBar.Enabled = false;
}
if (this.TotalContentHeight > this.Content.Height)
{
this.svScrollBar.Enabled = true;
this.svScrollBar.Maximum = this.TotalContentHeight;
}
else
{
this.svScrollBar.Enabled = false;
}
// this must be set after the maximum is determined
this.shScrollBar.LargeChange = this.shScrollBar.Width;
this.shScrollBar.SmallChange = this.shScrollBar.LargeChange / 10;
this.svScrollBar.LargeChange = this.svScrollBar.Height;
this.svScrollBar.SmallChange = this.svScrollBar.LargeChange / 10;
}
}
}
Example content:
public class ExampleContent : Control
{
public ExampleContent()
{
this.DoubleBuffered = true;
}
static Random random = new Random();
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var graphics = e.Graphics;
// random color to make the clip rectangle visible in an unobtrusive way
var color = Color.FromArgb(random.Next(160, 180), random.Next(160, 180), random.Next(160, 180));
graphics.Clear(color);
Debug.WriteLine(this.AutoScrollOffset.X.ToString() + ", " + this.AutoScrollOffset.Y.ToString());
CheckerboardRenderer.DrawCheckerboard(
graphics,
this.AutoScrollOffset,
e.ClipRectangle,
new Size(50, 50)
);
StaticBoxRenderer.DrawBoxes(graphics, new Point(0, this.AutoScrollOffset.Y), 100, 30);
}
}
public static class CheckerboardRenderer
{
public static void DrawCheckerboard(Graphics g, Point origin, Rectangle bounds, Size squareSize)
{
var numSquaresH = (bounds.Width + squareSize.Width - 1) / squareSize.Width + 1;
var numSquaresV = (bounds.Height + squareSize.Height - 1) / squareSize.Height + 1;
var startBoxH = (bounds.X - origin.X) / squareSize.Width;
var startBoxV = (bounds.Y - origin.Y) / squareSize.Height;
for (int i = startBoxH; i < startBoxH + numSquaresH; i++)
{
for (int j = startBoxV; j < startBoxV + numSquaresV; j++)
{
if ((i + j) % 2 == 0)
{
Random random = new Random(i * j);
var color = Color.FromArgb(random.Next(70, 95), random.Next(70, 95), random.Next(70, 95));
var brush = new SolidBrush(color);
g.FillRectangle(brush, i * squareSize.Width + origin.X, j * squareSize.Height + origin.Y, squareSize.Width, squareSize.Height);
brush.Dispose();
}
}
}
}
}
public static class StaticBoxRenderer
{
public static void DrawBoxes(Graphics g, Point origin, int boxWidth, int boxHeight)
{
int height = origin.Y;
int left = origin.X;
for (int i = 0; i < 25; i++)
{
Rectangle r = new Rectangle(left, height, boxWidth, boxHeight);
g.FillRectangle(Brushes.White, r);
g.DrawRectangle(Pens.Black, r);
height += boxHeight;
}
}
}