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: }