// "Therefore those skilled at the unorthodox // are infinite as heaven and earth, // inexhaustible as the great rivers. // When they come to an end, // they begin again, // like the days and months; // they die and are reborn, // like the four seasons." // // - Sun Tsu, // "The Art of War" using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Text; using System.Windows.Forms; using TheArtOfDev.HtmlRenderer.Core; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.WinForms.Utilities; namespace TheArtOfDev.HtmlRenderer.WinForms { /// /// Provides HTML rendering on the tooltips. /// public class HtmlToolTip : ToolTip { #region Fields and Consts /// /// the container to render and handle the html shown in the tooltip /// protected HtmlContainer _htmlContainer; /// /// the raw base stylesheet data used in the control /// protected string _baseRawCssData; /// /// the base stylesheet data used in the panel /// protected CssData _baseCssData; /// /// The text rendering hint to be used for text rendering. /// protected TextRenderingHint _textRenderingHint = TextRenderingHint.SystemDefault; /// /// The CSS class used for tooltip html root div /// private string _tooltipCssClass = "htmltooltip"; #if !MONO /// /// the control that the tooltip is currently showing on.
/// Used for link handling. ///
private Control _associatedControl; /// /// timer used to handle mouse move events when mouse is over the tooltip.
/// Used for link handling. ///
private Timer _linkHandlingTimer; /// /// the handle of the actual tooltip window used to know when the tooltip is hidden
/// Used for link handling. ///
private IntPtr _tooltipHandle; /// /// If to handle links in the tooltip (default: false).
/// When set to true the mouse pointer will change to hand when hovering over a tooltip and /// if clicked the event will be raised although the tooltip will be closed. ///
private bool _allowLinksHandling = true; #endif #endregion /// /// Init. /// public HtmlToolTip() { OwnerDraw = true; _htmlContainer = new HtmlContainer(); _htmlContainer.IsSelectionEnabled = false; _htmlContainer.IsContextMenuEnabled = false; _htmlContainer.AvoidGeometryAntialias = true; _htmlContainer.AvoidImagesLateLoading = true; _htmlContainer.RenderError += OnRenderError; _htmlContainer.StylesheetLoad += OnStylesheetLoad; _htmlContainer.ImageLoad += OnImageLoad; Popup += OnToolTipPopup; Draw += OnToolTipDraw; Disposed += OnToolTipDisposed; #if !MONO _linkHandlingTimer = new Timer(); _linkHandlingTimer.Tick += OnLinkHandlingTimerTick; _linkHandlingTimer.Interval = 40; _htmlContainer.LinkClicked += OnLinkClicked; #endif } #if !MONO /// /// Raised when the user clicks on a link in the html.
/// Allows canceling the execution of the link. ///
public event EventHandler LinkClicked; #endif /// /// Raised when an error occurred during html rendering.
///
public event EventHandler RenderError; /// /// Raised when aa stylesheet is about to be loaded by file path or URI by link element.
/// This event allows to provide the stylesheet manually or provide new source (file or uri) to load from.
/// If no alternative data is provided the original source will be used.
///
public event EventHandler StylesheetLoad; /// /// Raised when an image is about to be loaded by file path or URI.
/// This event allows to provide the image manually, if not handled the image will be loaded from file or download from URI. ///
public event EventHandler ImageLoad; /// /// Use GDI+ text rendering to measure/draw text.
///
/// /// /// GDI+ text rendering is less smooth than GDI text rendering but it natively supports alpha channel /// thus allows creating transparent images. /// /// /// While using GDI+ text rendering you can control the text rendering using , note that /// using doesn't work well with transparent background. /// /// [Category("Behavior")] [DefaultValue(false)] [EditorBrowsable(EditorBrowsableState.Always)] [Description("If to use GDI+ text rendering to measure/draw text, false - use GDI")] public bool UseGdiPlusTextRendering { get { return _htmlContainer.UseGdiPlusTextRendering; } set { _htmlContainer.UseGdiPlusTextRendering = value; } } /// /// The text rendering hint to be used for text rendering. /// [Category("Behavior")] [EditorBrowsable(EditorBrowsableState.Always)] [DefaultValue(TextRenderingHint.SystemDefault)] [Description("The text rendering hint to be used for text rendering.")] public TextRenderingHint TextRenderingHint { get { return _textRenderingHint; } set { _textRenderingHint = value; } } /// /// Set base stylesheet to be used by html rendered in the panel. /// [Browsable(true)] [Description("Set base stylesheet to be used by html rendered in the tooltip.")] [Category("Appearance")] [Editor("System.ComponentModel.Design.MultilineStringEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.Drawing.Design.UITypeEditor, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")] public virtual string BaseStylesheet { get { return _baseRawCssData; } set { _baseRawCssData = value; _baseCssData = HtmlRender.ParseStyleSheet(value); } } /// /// The CSS class used for tooltip html root div (default: htmltooltip)
/// Setting to 'null' clear base style on the tooltip.
/// Set custom class found in to change the base style of the tooltip. ///
[Browsable(true)] [Description("The CSS class used for tooltip html root div.")] [Category("Appearance")] public virtual string TooltipCssClass { get { return _tooltipCssClass; } set { _tooltipCssClass = value; } } #if !MONO /// /// If to handle links in the tooltip (default: false).
/// When set to true the mouse pointer will change to hand when hovering over a tooltip and /// if clicked the event will be raised although the tooltip will be closed. ///
[Browsable(true)] [DefaultValue(false)] [Description("If to handle links in the tooltip.")] [Category("Behavior")] public virtual bool AllowLinksHandling { get { return _allowLinksHandling; } set { _allowLinksHandling = value; } } #endif /// /// Gets or sets the max size the tooltip. /// /// An ordered pair of type representing the width and height of a rectangle. [Browsable(true)] [Category("Layout")] [Description("Restrict the max size of the shown tooltip (0 is not restricted)")] public virtual Size MaximumSize { get { return Size.Round(_htmlContainer.MaxSize); } set { _htmlContainer.MaxSize = value; } } #region Private methods /// /// On tooltip appear set the html by the associated control, layout and set the tooltip size by the html size. /// protected virtual void OnToolTipPopup(PopupEventArgs e) { //Create fragment container var cssClass = string.IsNullOrEmpty(_tooltipCssClass) ? null : string.Format(" class=\"{0}\"", _tooltipCssClass); var toolipHtml = string.Format("{1}", cssClass, GetToolTip(e.AssociatedControl)); _htmlContainer.SetHtml(toolipHtml, _baseCssData); _htmlContainer.MaxSize = MaximumSize; //Measure size of the container using (var g = e.AssociatedControl.CreateGraphics()) { g.TextRenderingHint = _textRenderingHint; _htmlContainer.PerformLayout(g); } //Set the size of the tooltip var desiredWidth = (int)Math.Ceiling(MaximumSize.Width > 0 ? Math.Min(_htmlContainer.ActualSize.Width, MaximumSize.Width) : _htmlContainer.ActualSize.Width); var desiredHeight = (int)Math.Ceiling(MaximumSize.Height > 0 ? Math.Min(_htmlContainer.ActualSize.Height, MaximumSize.Height) : _htmlContainer.ActualSize.Height); e.ToolTipSize = new Size(desiredWidth, desiredHeight); #if !MONO // start mouse handle timer if (_allowLinksHandling) { _associatedControl = e.AssociatedControl; _linkHandlingTimer.Start(); } #endif } /// /// Draw the html using the tooltip graphics. /// protected virtual void OnToolTipDraw(DrawToolTipEventArgs e) { #if !MONO if (_tooltipHandle == IntPtr.Zero) { // get the handle of the tooltip window using the graphics device context var hdc = e.Graphics.GetHdc(); _tooltipHandle = Win32Utils.WindowFromDC(hdc); e.Graphics.ReleaseHdc(hdc); AdjustTooltipPosition(e.AssociatedControl, e.Bounds.Size); } #endif e.Graphics.Clear(Color.White); e.Graphics.TextRenderingHint = _textRenderingHint; _htmlContainer.PerformPaint(e.Graphics); } /// /// Adjust the location of the tooltip window to the location of the mouse and handle /// if the tooltip window will try to appear outside the boundaries of the control. /// /// the control the tooltip is appearing on /// the size of the tooltip window protected virtual void AdjustTooltipPosition(Control associatedControl, Size size) { var mousePos = Control.MousePosition; var screenBounds = Screen.FromControl(associatedControl).WorkingArea; // adjust if tooltip is outside form bounds if (mousePos.X + size.Width > screenBounds.Right) mousePos.X = Math.Max(screenBounds.Right - size.Width - 5, screenBounds.Left + 3); const int yOffset = 20; if (mousePos.Y + size.Height + yOffset > screenBounds.Bottom) mousePos.Y = Math.Max(screenBounds.Bottom - size.Height - yOffset - 3, screenBounds.Top + 2); #if !MONO // move the tooltip window to new location Win32Utils.MoveWindow(_tooltipHandle, mousePos.X, mousePos.Y + yOffset, size.Width, size.Height, false); #endif } #if !MONO /// /// Propagate the LinkClicked event from root container. /// protected virtual void OnLinkClicked(HtmlLinkClickedEventArgs e) { var handler = LinkClicked; if (handler != null) handler(this, e); } #endif /// /// Propagate the Render Error event from root container. /// protected virtual void OnRenderError(HtmlRenderErrorEventArgs e) { var handler = RenderError; if (handler != null) handler(this, e); } /// /// Propagate the stylesheet load event from root container. /// protected virtual void OnStylesheetLoad(HtmlStylesheetLoadEventArgs e) { var handler = StylesheetLoad; if (handler != null) handler(this, e); } /// /// Propagate the image load event from root container. /// protected virtual void OnImageLoad(HtmlImageLoadEventArgs e) { var handler = ImageLoad; if (handler != null) handler(this, e); } #if !MONO /// /// Raised on link handling timer tick, used for: /// 1. Know when the tooltip is hidden by checking the visibility of the tooltip window. /// 2. Call HandleMouseMove so the mouse cursor will react if over a link element. /// 3. Call HandleMouseDown and HandleMouseUp to simulate click on a link if one was clicked. /// protected virtual void OnLinkHandlingTimerTick(EventArgs e) { try { var handle = _tooltipHandle; if (handle != IntPtr.Zero && Win32Utils.IsWindowVisible(handle)) { var mPos = Control.MousePosition; var mButtons = Control.MouseButtons; var rect = Win32Utils.GetWindowRectangle(handle); if (rect.Contains(mPos)) { _htmlContainer.HandleMouseMove(_associatedControl, new MouseEventArgs(mButtons, 0, mPos.X - rect.X, mPos.Y - rect.Y, 0)); } } else { _linkHandlingTimer.Stop(); _tooltipHandle = IntPtr.Zero; var mPos = Control.MousePosition; var mButtons = Control.MouseButtons; var rect = Win32Utils.GetWindowRectangle(handle); if (rect.Contains(mPos)) { if (mButtons == MouseButtons.Left) { var args = new MouseEventArgs(mButtons, 1, mPos.X - rect.X, mPos.Y - rect.Y, 0); _htmlContainer.HandleMouseDown(_associatedControl, args); _htmlContainer.HandleMouseUp(_associatedControl, args); } } } } catch (Exception ex) { OnRenderError(this, new HtmlRenderErrorEventArgs(HtmlRenderErrorType.General, "Error in link handling for tooltip", ex)); } } #endif /// /// Unsubscribe from events and dispose of . /// protected virtual void OnToolTipDisposed(EventArgs e) { Popup -= OnToolTipPopup; Draw -= OnToolTipDraw; Disposed -= OnToolTipDisposed; if (_htmlContainer != null) { _htmlContainer.RenderError -= OnRenderError; _htmlContainer.StylesheetLoad -= OnStylesheetLoad; _htmlContainer.ImageLoad -= OnImageLoad; _htmlContainer.Dispose(); _htmlContainer = null; } #if !MONO if (_linkHandlingTimer != null) { _linkHandlingTimer.Dispose(); _linkHandlingTimer = null; if (_htmlContainer != null) _htmlContainer.LinkClicked -= OnLinkClicked; } #endif } #region Private event handlers private void OnToolTipPopup(object sender, PopupEventArgs e) { OnToolTipPopup(e); } private void OnToolTipDraw(object sender, DrawToolTipEventArgs e) { OnToolTipDraw(e); } private void OnRenderError(object sender, HtmlRenderErrorEventArgs e) { OnRenderError(e); } private void OnStylesheetLoad(object sender, HtmlStylesheetLoadEventArgs e) { OnStylesheetLoad(e); } private void OnImageLoad(object sender, HtmlImageLoadEventArgs e) { OnImageLoad(e); } #if !MONO private void OnLinkClicked(object sender, HtmlLinkClickedEventArgs e) { OnLinkClicked(e); } private void OnLinkHandlingTimerTick(object sender, EventArgs e) { OnLinkHandlingTimerTick(e); } #endif private void OnToolTipDisposed(object sender, EventArgs e) { OnToolTipDisposed(e); } #endregion #endregion } }