StephaneLenclud@123: // StephaneLenclud@123: // Copyright (C) 2014-2015 Stéphane Lenclud. StephaneLenclud@123: // StephaneLenclud@123: // This file is part of SharpDisplayManager. StephaneLenclud@123: // StephaneLenclud@123: // SharpDisplayManager is free software: you can redistribute it and/or modify StephaneLenclud@123: // it under the terms of the GNU General Public License as published by StephaneLenclud@123: // the Free Software Foundation, either version 3 of the License, or StephaneLenclud@123: // (at your option) any later version. StephaneLenclud@123: // StephaneLenclud@123: // SharpDisplayManager is distributed in the hope that it will be useful, StephaneLenclud@123: // but WITHOUT ANY WARRANTY; without even the implied warranty of StephaneLenclud@123: // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the StephaneLenclud@123: // GNU General Public License for more details. StephaneLenclud@123: // StephaneLenclud@123: // You should have received a copy of the GNU General Public License StephaneLenclud@123: // along with SharpDisplayManager. If not, see . StephaneLenclud@123: // StephaneLenclud@123: StephaneLenclud@123: using System; sl@0: using System.Collections.Generic; sl@0: using System.ComponentModel; sl@0: using System.Diagnostics; sl@0: using System.Linq; sl@0: using System.Text; sl@0: using System.Threading.Tasks; sl@0: //using System.Timers; sl@0: using System.Windows.Forms; sl@0: using System.Drawing; sl@0: sl@0: namespace SharpDisplayManager sl@0: { sl@15: [System.ComponentModel.DesignerCategory("Code")] sl@17: public class MarqueeLabel : Label sl@0: { sl@2: private bool iOwnTimer; sl@5: private StringFormat iStringFormat; sl@5: private SolidBrush iBrush; sl@6: private SizeF iTextSize; sl@8: private SizeF iSeparatorSize; sl@34: private SizeF iScrollSize; sl@100: private Font iFontInUse; StephaneLenclud@110: private string iSeparator; sl@8: sl@8: [Category("Appearance")] sl@8: [Description("Separator in our scrolling loop.")] sl@8: [DefaultValue(" | ")] sl@8: [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)] sl@11: [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] StephaneLenclud@110: public string Separator StephaneLenclud@110: { StephaneLenclud@110: get StephaneLenclud@110: { StephaneLenclud@110: return iSeparator; StephaneLenclud@110: } StephaneLenclud@110: set StephaneLenclud@110: { StephaneLenclud@110: if (value != Separator) StephaneLenclud@110: { StephaneLenclud@110: iSeparator = value; StephaneLenclud@110: OnTextChanged(EventArgs.Empty); StephaneLenclud@110: } StephaneLenclud@110: } StephaneLenclud@110: } sl@0: sl@0: [Category("Behavior")] sl@0: [Description("How fast is our text scrolling, in pixels per second.")] sl@0: [DefaultValue(32)] sl@0: [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)] sl@11: [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] sl@0: public int PixelsPerSecond { get; set; } sl@0: sl@2: [Category("Behavior")] sl@100: [Description("Should we scale down our font to try fit our text without scrolling.")] sl@100: [DefaultValue(false)] sl@100: [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)] sl@100: [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] sl@100: public bool ScaleToFit { get; set; } sl@100: sl@100: [Category("Behavior")] sl@100: [Description("Minimum size of our font allowed when scaling is enabled.")] sl@100: [DefaultValue(15)] sl@100: [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)] sl@100: [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] sl@100: public float MinFontSize { get; set; } sl@100: sl@100: [Category("Behavior")] sl@2: [Description("Use an internal or an external timer.")] sl@2: [DefaultValue(true)] sl@2: [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)] sl@11: [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] sl@2: public bool OwnTimer sl@2: { sl@2: get sl@2: { sl@2: return iOwnTimer; sl@2: } sl@2: set sl@2: { sl@2: iOwnTimer = value; sl@2: sl@2: if (iOwnTimer) sl@2: { sl@2: Timer = new Timer(); sl@2: Timer.Interval = 10; sl@2: Timer.Tick += new EventHandler(Timer_Tick); sl@2: Timer.Start(); sl@2: } sl@2: else sl@2: { sl@2: if (Timer != null) sl@2: Timer.Dispose(); sl@2: Timer = null; sl@2: } sl@2: sl@2: } sl@2: } sl@5: sl@2: private int CurrentPosition { get; set; } sl@2: private Timer Timer { get; set; } sl@0: private DateTime LastTickTime { get; set; } sl@0: private double PixelsLeft { get; set; } sl@0: //DateTime a = new DateTime(2010, 05, 12, 13, 15, 00); sl@2: //DateTime b = new DateTime(2010, 05, 12, 13, 45, 00); StephaneLenclud@253: //Trace.WriteLine(b.Subtract(a).TotalMinutes); sl@0: sl@0: public MarqueeLabel() sl@0: { sl@0: UseCompatibleTextRendering = true; sl@0: //PixelsPerSecond = 32; sl@0: LastTickTime = DateTime.Now; sl@0: PixelsLeft = 0; sl@33: CurrentPosition = 0; sl@5: iBrush = new SolidBrush(ForeColor); sl@100: MinFontSize = 15; sl@100: ScaleToFit = true; sl@100: //Just clone our font sl@100: iFontInUse = new Font(Font, Font.Style); StephaneLenclud@35: sl@41: //Following is needed if we ever switch from Label to Control base class. StephaneLenclud@35: //Without it you get some pretty nasty flicker StephaneLenclud@35: //SetStyle(ControlStyles.OptimizedDoubleBuffer, true); StephaneLenclud@35: //SetStyle(ControlStyles.UserPaint, true); sl@41: //SetStyle(ControlStyles.AllPaintingInWmPaint, true); StephaneLenclud@35: //SetStyle(ControlStyles.DoubleBuffer, true); sl@0: } sl@0: sl@2: public void UpdateAnimation(DateTime aLastTickTime, DateTime aNewTickTime) sl@0: { sl@6: if (!NeedToScroll()) sl@0: { sl@6: CurrentPosition = 0; sl@6: return; sl@6: } sl@6: sl@34: /* sl@8: while (CurrentPosition > (iTextSize.Width + iSeparatorSize.Width)) sl@6: { sl@8: CurrentPosition -= ((int)(iTextSize.Width + iSeparatorSize.Width)); sl@0: } sl@34: */ sl@34: sl@34: while (CurrentPosition > iScrollSize.Width) sl@34: { sl@34: CurrentPosition -= (int)iScrollSize.Width; sl@34: } sl@34: sl@0: sl@2: PixelsLeft += aNewTickTime.Subtract(aLastTickTime).TotalSeconds * PixelsPerSecond; sl@0: sl@0: //Keep track of our pixels left over sl@0: //PixelsLeft = offset - Math.Truncate(offset); sl@0: double offset = Math.Truncate(PixelsLeft); sl@0: PixelsLeft -= offset; sl@0: sl@0: CurrentPosition += Convert.ToInt32(offset); sl@0: sl@0: /* sl@0: if (offset > 1.0) sl@0: { sl@0: BackColor = Color.Red; sl@0: } sl@0: else if (offset==1.0) sl@0: { sl@0: if (BackColor != Color.White) sl@0: { sl@0: BackColor = Color.White; sl@0: } sl@0: sl@0: } sl@0: else sl@0: { sl@0: //Too slow sl@0: //BackColor = Color.Green; sl@0: }*/ sl@0: sl@0: //Only redraw if something has changed sl@0: if (offset != 0) sl@0: { sl@0: Invalidate(); sl@0: } sl@2: } sl@0: sl@2: void Timer_Tick(object sender, EventArgs e) sl@2: { sl@5: DateTime NewTickTime = DateTime.Now; sl@2: // sl@2: UpdateAnimation(LastTickTime, NewTickTime); sl@2: // sl@2: LastTickTime = NewTickTime; sl@0: } sl@0: sl@5: private StringFormat GetStringFormatFromContentAllignment(ContentAlignment ca) sl@5: { sl@41: StringFormat format = new StringFormat(StringFormat.GenericTypographic); sl@5: switch (ca) sl@5: { sl@5: case ContentAlignment.TopCenter: sl@34: format.Alignment = StringAlignment.Center; sl@34: format.LineAlignment = StringAlignment.Near; sl@5: break; sl@5: case ContentAlignment.TopLeft: sl@5: format.Alignment = StringAlignment.Near; sl@5: format.LineAlignment = StringAlignment.Near; sl@5: break; sl@5: case ContentAlignment.TopRight: sl@34: format.Alignment = StringAlignment.Far; sl@34: format.LineAlignment = StringAlignment.Near; sl@5: break; sl@5: case ContentAlignment.MiddleCenter: sl@5: format.Alignment = StringAlignment.Center; sl@5: format.LineAlignment = StringAlignment.Center; sl@5: break; sl@5: case ContentAlignment.MiddleLeft: sl@34: format.Alignment = StringAlignment.Near; sl@34: format.LineAlignment = StringAlignment.Center; sl@5: break; sl@5: case ContentAlignment.MiddleRight: sl@34: format.Alignment = StringAlignment.Far; sl@34: format.LineAlignment = StringAlignment.Center; sl@34: break; sl@34: case ContentAlignment.BottomCenter: sl@5: format.Alignment = StringAlignment.Center; sl@5: format.LineAlignment = StringAlignment.Far; sl@5: break; sl@5: case ContentAlignment.BottomLeft: sl@34: format.Alignment = StringAlignment.Near; sl@34: format.LineAlignment = StringAlignment.Far; sl@5: break; sl@5: case ContentAlignment.BottomRight: sl@5: format.Alignment = StringAlignment.Far; sl@5: format.LineAlignment = StringAlignment.Far; sl@5: break; sl@5: } sl@5: sl@5: format.FormatFlags |= StringFormatFlags.NoWrap; sl@5: format.FormatFlags |= StringFormatFlags.NoClip; sl@11: format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; sl@5: format.Trimming = StringTrimming.None; sl@5: sl@5: return format; sl@5: } sl@5: sl@5: protected override void OnForeColorChanged(EventArgs e) sl@5: { sl@6: //Color has changed recreate our brush sl@5: iBrush = new SolidBrush(ForeColor); sl@5: sl@5: base.OnForeColorChanged(e); sl@5: } sl@5: sl@6: sl@100: private void ComputeSizes() sl@6: { sl@11: //For all string measurements and drawing issues refer to the following article: sl@11: // http://stackoverflow.com/questions/1203087/why-is-graphics-measurestring-returning-a-higher-than-expected-number sl@6: //Update text size according to text and font sl@6: Graphics g = this.CreateGraphics(); sl@6: g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixelGridFit; sl@6: iStringFormat = GetStringFormatFromContentAllignment(TextAlign); sl@100: iTextSize = g.MeasureString(Text, iFontInUse, Int32.MaxValue, iStringFormat); sl@100: iSeparatorSize = g.MeasureString(Separator, iFontInUse, Int32.MaxValue, iStringFormat); sl@34: //Scroll width is the width of our text and our separator without taking kerning into account since sl@34: //both text and separator are drawn independently from each other. sl@34: iScrollSize.Width = iSeparatorSize.Width + iTextSize.Width; sl@34: iScrollSize.Height = Math.Max(iSeparatorSize.Height, iTextSize.Height); //Not relevant for now sl@100: //We don't want scroll width to take kerning into account so we don't use the following sl@34: //iScrollSize = g.MeasureString(Text + Separator, Font, Int32.MaxValue, iStringFormat); sl@100: } sl@100: sl@100: private void HandleTextSizeChange() sl@100: { sl@100: ComputeSizes(); sl@6: sl@6: if (NeedToScroll()) sl@6: { sl@100: if (ScaleToFit && iFontInUse.SizeInPoints > MinFontSize) sl@100: { sl@100: //Try scaling down sl@100: iFontInUse = new Font(Font.FontFamily, iFontInUse.SizeInPoints - 1, Font.Style); sl@100: //Recurse until we are done sl@100: HandleTextSizeChange(); sl@100: } sl@100: else sl@100: { sl@100: if (ScaleToFit) sl@100: { sl@100: //Our minimum font size still needs scrolling sl@100: //Reset our font then sl@100: iFontInUse = new Font(Font,Font.Style); sl@100: ComputeSizes(); sl@100: } sl@100: sl@100: //Scrolling is ok or needed sl@100: //Always align left when scrolling sl@100: iStringFormat.Alignment = StringAlignment.Near; sl@100: } sl@41: } sl@42: sl@100: //Reset our timer whenever our text changes sl@100: CurrentPosition = 0; sl@100: PixelsLeft = 0; sl@100: LastTickTime = DateTime.Now; sl@6: } sl@6: sl@6: protected override void OnTextChanged(EventArgs e) sl@6: { sl@100: //Just clone our font sl@100: iFontInUse = new Font(Font, Font.Style); sl@100: sl@6: HandleTextSizeChange(); sl@6: sl@6: base.OnTextChanged(e); sl@6: } sl@6: sl@6: protected override void OnFontChanged(EventArgs e) sl@6: { sl@100: //Just clone our font sl@100: iFontInUse = new Font(Font,Font.Style); sl@100: sl@6: HandleTextSizeChange(); sl@6: sl@6: base.OnFontChanged(e); sl@6: } sl@6: sl@100: protected override void OnSizeChanged(EventArgs e) sl@100: { sl@100: //Just clone our font sl@100: iFontInUse = new Font(Font, Font.Style); sl@100: sl@100: HandleTextSizeChange(); sl@100: sl@100: base.OnSizeChanged(e); sl@100: } sl@100: sl@5: protected override void OnTextAlignChanged(EventArgs e) sl@5: { sl@42: iStringFormat = GetStringFormatFromContentAllignment(TextAlign); sl@41: if (NeedToScroll()) sl@41: { sl@41: //Always align left when scrolling to avoid bugs sl@42: iStringFormat.Alignment = StringAlignment.Near; sl@41: } sl@42: sl@41: Invalidate(); sl@41: // sl@5: base.OnTextAlignChanged(e); sl@5: } sl@5: sl@0: protected override void OnPaint(PaintEventArgs e) sl@0: { sl@0: //Disable anti-aliasing sl@0: e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixelGridFit; sl@6: if (NeedToScroll()) sl@6: { sl@34: //Draw it all in a single call would take kerning into account sl@34: //e.Graphics.TranslateTransform(-(float)CurrentPosition, 0); sl@41: //e.Graphics.DrawString(Text + Separator + Text, Font, iBrush, ClientRectangle, StringFormat); sl@34: sl@34: //Doing separate draw operation allows us not to take kerning into account between separator and string sl@34: //Draw the first one sl@33: e.Graphics.TranslateTransform(-(float)CurrentPosition, 0); sl@100: e.Graphics.DrawString(Text, iFontInUse, iBrush, ClientRectangle, iStringFormat); sl@8: //Draw separator sl@34: e.Graphics.TranslateTransform(iTextSize.Width, 0); sl@100: e.Graphics.DrawString(Separator, iFontInUse, iBrush, ClientRectangle, iStringFormat); sl@6: //Draw the last one sl@34: e.Graphics.TranslateTransform(iSeparatorSize.Width, 0); sl@100: e.Graphics.DrawString(Text, iFontInUse, iBrush, ClientRectangle, iStringFormat); sl@6: } sl@6: else sl@6: { sl@100: e.Graphics.DrawString(Text, iFontInUse, iBrush, ClientRectangle, iStringFormat); sl@6: } sl@6: sl@6: sl@5: sl@5: //DrawText is not working without anti-aliasing. See: stackoverflow.com/questions/8283631/graphics-drawstring-vs-textrenderer-drawtextwhich-can-deliver-better-quality sl@5: //TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, ForeColor, BackColor, iTextFormatFlags); sl@5: sl@5: //base.OnPaint(e); sl@0: } sl@0: sl@6: public bool NeedToScroll() sl@6: { sl@6: //if (Width < e.Graphics.MeasureString(Text, Font).Width) sl@6: if (Width < iTextSize.Width) sl@6: { sl@6: return true; sl@6: } sl@6: return false; sl@6: } sl@6: StephaneLenclud@278: /// StephaneLenclud@278: /// See Dispose Pattern reference: StephaneLenclud@278: /// https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 StephaneLenclud@278: /// StephaneLenclud@278: /// sl@0: protected override void Dispose(bool disposing) sl@0: { StephaneLenclud@278: // My sure we hook the rest of the framework. StephaneLenclud@278: // That's most important for controls since they do things like removing themselves from containers upon disposal. StephaneLenclud@278: base.Dispose(disposing); StephaneLenclud@278: // My understanding is that one is not supose to Dispose other objects unless disposing is true. sl@0: if (disposing) sl@0: { sl@0: if (Timer != null) StephaneLenclud@278: { sl@0: Timer.Dispose(); StephaneLenclud@278: } sl@0: } sl@0: Timer = null; sl@0: } sl@0: } sl@0: }