// "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.Diagnostics; using System.Drawing; using System.Drawing.Text; using System.Windows.Forms; using TheArtOfDev.HtmlRenderer.Core; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Core.Utils; using TheArtOfDev.HtmlRenderer.WinForms.Utilities; namespace TheArtOfDev.HtmlRenderer.WinForms { /// /// Provides HTML rendering using the text property.
/// WinForms control that will render html content in it's client rectangle.
/// If is true and the layout of the html resulted in its content beyond the client bounds /// of the panel it will show scrollbars (horizontal/vertical) allowing to scroll the content.
/// If is false html content outside the client bounds will be clipped.
/// The control will handle mouse and keyboard events on it to support html text selection, copy-paste and mouse clicks.
/// /// The major differential to use HtmlPanel or HtmlLabel is size and scrollbars.
/// If the size of the control depends on the html content the HtmlLabel should be used.
/// If the size is set by some kind of layout then HtmlPanel is more suitable, also shows scrollbars if the html contents is larger than the control client rectangle.
///
/// ///

AutoScroll:

/// Allows showing scrollbars if html content is placed outside the visible boundaries of the panel. ///
/// ///

LinkClicked event:

/// Raised when the user clicks on a link in the html.
/// Allows canceling the execution of the link. ///
/// ///

StylesheetLoad event:

/// Raised when a 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.
///
/// ///

ImageLoad event:

/// 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. ///
/// ///

RenderError event:

/// Raised when an error occurred during html rendering.
///
///
public class HtmlPanel : ScrollableControl { #region Fields and Consts /// /// Underline html container instance. /// protected HtmlContainer _htmlContainer; /// /// The current border style of the control /// protected BorderStyle _borderStyle; /// /// the raw base stylesheet data used in the control /// protected string _baseRawCssData; /// /// the base stylesheet data used in the control /// protected CssData _baseCssData; /// /// the current html text set in the control /// protected string _text; /// /// If to use cursors defined by the operating system or .NET cursors /// protected bool _useSystemCursors; /// /// The text rendering hint to be used for text rendering. /// protected TextRenderingHint _textRenderingHint = TextRenderingHint.SystemDefault; /// /// The last position of the scrollbars to know if it has changed to update mouse /// protected Point _lastScrollOffset; #endregion /// /// Creates a new HtmlPanel and sets a basic css for it's styling. /// public HtmlPanel() { AutoScroll = true; BackColor = SystemColors.Window; DoubleBuffered = true; SetStyle(ControlStyles.ResizeRedraw, true); SetStyle(ControlStyles.SupportsTransparentBackColor, true); _htmlContainer = new HtmlContainer(); _htmlContainer.LinkClicked += OnLinkClicked; _htmlContainer.RenderError += OnRenderError; _htmlContainer.Refresh += OnRefresh; _htmlContainer.ScrollChange += OnScrollChange; _htmlContainer.StylesheetLoad += OnStylesheetLoad; _htmlContainer.ImageLoad += OnImageLoad; } /// /// /// public int PageCount { get { return _htmlContainer.PageListCount; } } /// /// Raised when the BorderStyle property value changes. /// [Category("Property Changed")] public event EventHandler BorderStyleChanged; /// /// Raised when the user clicks on a link in the html.
/// Allows canceling the execution of the link. ///
public event EventHandler LinkClicked; /// /// Raised when an error occurred during html rendering.
///
public event EventHandler RenderError; /// /// Raised when a 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; /// /// Gets or sets a value indicating if anti-aliasing should be avoided for geometry like backgrounds and borders (default - false). /// [Category("Behavior")] [DefaultValue(false)] [Description("If anti-aliasing should be avoided for geometry like backgrounds and borders")] public virtual bool AvoidGeometryAntialias { get { return _htmlContainer.AvoidGeometryAntialias; } set { _htmlContainer.AvoidGeometryAntialias = value; } } /// /// Gets or sets a value indicating if image loading only when visible should be avoided (default - false).
/// True - images are loaded as soon as the html is parsed.
/// False - images that are not visible because of scroll location are not loaded until they are scrolled to. ///
/// /// Images late loading improve performance if the page contains image outside the visible scroll area, especially if there is large /// amount of images, as all image loading is delayed (downloading and loading into memory).
/// Late image loading may effect the layout and actual size as image without set size will not have actual size until they are loaded /// resulting in layout change during user scroll.
/// Early image loading may also effect the layout if image without known size above the current scroll location are loaded as they /// will push the html elements down. ///
[Category("Behavior")] [DefaultValue(false)] [Description("If image loading only when visible should be avoided")] public virtual bool AvoidImagesLateLoading { get { return _htmlContainer.AvoidImagesLateLoading; } set { _htmlContainer.AvoidImagesLateLoading = value; } } /// /// 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; } } /// /// If to use cursors defined by the operating system or .NET cursors /// [Category("Behavior")] [EditorBrowsable(EditorBrowsableState.Always)] [DefaultValue(false)] [Description("If to use cursors defined by the operating system or .NET cursors")] public bool UseSystemCursors { get { return _useSystemCursors; } set { _useSystemCursors = value; } } /// /// Gets or sets the border style. /// /// The border style. [Category("Appearance")] [DefaultValue(typeof(BorderStyle), "None")] public virtual BorderStyle BorderStyle { get { return _borderStyle; } set { if (BorderStyle != value) { _borderStyle = value; OnBorderStyleChanged(EventArgs.Empty); } } } /// /// Is content selection is enabled for the rendered html (default - true).
/// If set to 'false' the rendered html will be static only with ability to click on links. ///
[Browsable(true)] [DefaultValue(true)] [Category("Behavior")] [EditorBrowsable(EditorBrowsableState.Always)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] [Description("Is content selection is enabled for the rendered html.")] public virtual bool IsSelectionEnabled { get { return _htmlContainer.IsSelectionEnabled; } set { _htmlContainer.IsSelectionEnabled = value; } } /// /// Is the build-in context menu enabled and will be shown on mouse right click (default - true) /// [Browsable(true)] [DefaultValue(true)] [Category("Behavior")] [EditorBrowsable(EditorBrowsableState.Always)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] [Description("Is the build-in context menu enabled and will be shown on mouse right click.")] public virtual bool IsContextMenuEnabled { get { return _htmlContainer.IsContextMenuEnabled; } set { _htmlContainer.IsContextMenuEnabled = value; } } /// /// Set base stylesheet to be used by html rendered in the panel. /// [Browsable(true)] [Category("Appearance")] [Description("Set base stylesheet to be used by html rendered in the control.")] [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); _htmlContainer.SetHtml(_text, _baseCssData); } } /// /// Gets or sets a value indicating whether the container enables the user to scroll to any controls placed outside of its visible boundaries. /// [Browsable(true)] [Description("Sets a value indicating whether the container enables the user to scroll to any controls placed outside of its visible boundaries.")] public override bool AutoScroll { get { return base.AutoScroll; } set { base.AutoScroll = value; } } /// /// Gets or sets the text of this panel /// [Browsable(true)] [Description("Sets the html of this control.")] public override string Text { get { return _text; } set { _text = value; base.Text = value; if (!IsDisposed) { VerticalScroll.Value = VerticalScroll.Minimum; _htmlContainer.SetHtml(_text, _baseCssData); Refresh(); } } } /// /// /// public override void Refresh() { if (!IsDisposed) { PerformLayout(); Invalidate(); InvokeMouseMove(); } } /// /// Get the currently selected text segment in the html. /// [Browsable(false)] public virtual string SelectedText { get { return _htmlContainer.SelectedText; } } /// /// Copy the currently selected html segment with style. /// [Browsable(false)] public virtual string SelectedHtml { get { return _htmlContainer.SelectedHtml; } } /// /// Get html from the current DOM tree with inline style. /// /// generated html public virtual string GetHtml() { return _htmlContainer != null ? _htmlContainer.GetHtml() : null; } /// /// Get the rectangle of html element as calculated by html layout.
/// Element if found by id (id attribute on the html element).
/// Note: to get the screen rectangle you need to adjust by the hosting control.
///
/// the id of the element to get its rectangle /// the rectangle of the element or null if not found public virtual RectangleF? GetElementRectangle(string elementId) { return _htmlContainer != null ? _htmlContainer.GetElementRectangle(elementId) : null; } /// /// Adjust the scrollbar of the panel on html element by the given id.
/// The top of the html element rectangle will be at the top of the panel, if there /// is not enough height to scroll to the top the scroll will be at maximum.
///
/// the id of the element to scroll to public virtual void ScrollToElement(string elementId) { ArgChecker.AssertArgNotNullOrEmpty(elementId, "elementId"); if (_htmlContainer != null) { var rect = _htmlContainer.GetElementRectangle(elementId); if (rect.HasValue) { UpdateScroll(Point.Round(rect.Value.Location)); _htmlContainer.HandleMouseMove(this, new MouseEventArgs(MouseButtons, 0, MousePosition.X, MousePosition.Y, 0)); } } } #region Private methods #if !MONO /// /// Override to support border for the control. /// protected override CreateParams CreateParams { get { CreateParams createParams = base.CreateParams; switch (_borderStyle) { case BorderStyle.FixedSingle: createParams.Style |= Win32Utils.WsBorder; break; case BorderStyle.Fixed3D: createParams.ExStyle |= Win32Utils.WsExClientEdge; break; } return createParams; } } #endif /// /// Perform the layout of the html in the control. /// protected override void OnLayout(LayoutEventArgs levent) { PerformHtmlLayout(); base.OnLayout(levent); // to handle if vertical scrollbar is appearing or disappearing if (_htmlContainer != null && Math.Abs(_htmlContainer.MaxSize.Width - ClientSize.Width) > 0.1) { PerformHtmlLayout(); base.OnLayout(levent); } } /// /// Perform html container layout by the current panel client size. /// protected void PerformHtmlLayout() { if (_htmlContainer != null) { _htmlContainer.MaxSize = new SizeF(ClientSize.Width - Padding.Horizontal, 0); using (var g = CreateGraphics()) { _htmlContainer.PerformLayout(g); } if (_htmlContainer.PageListCount>0) { AutoScrollMinSize = Size.Round(new SizeF(_htmlContainer.PageList.Width, _htmlContainer.PageList.Height));// _htmlContainer.ActualSize.Width + Padding.Horizontal, _htmlContainer.ActualSize.Height)); } else { AutoScrollMinSize = Size.Round(new SizeF(_htmlContainer.ActualSize.Width + Padding.Horizontal, _htmlContainer.ActualSize.Height)); } } } /// /// Perform paint of the html in the control. /// protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (_htmlContainer != null) { e.Graphics.TextRenderingHint = _textRenderingHint; e.Graphics.SetClip(ClientRectangle); _htmlContainer.Location = new PointF(Padding.Left, Padding.Top); _htmlContainer.ScrollOffset = AutoScrollPosition; _htmlContainer.PerformPaint(e.Graphics); if (!_lastScrollOffset.Equals(_htmlContainer.ScrollOffset)) { _lastScrollOffset = _htmlContainer.ScrollOffset; InvokeMouseMove(); } } } /// /// Set focus on the control for keyboard scrollbars handling. /// protected override void OnClick(EventArgs e) { base.OnClick(e); Focus(); } /// /// Handle mouse move to handle hover cursor and text selection. /// protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (_htmlContainer != null) _htmlContainer.HandleMouseMove(this, e); } /// /// Handle mouse leave to handle cursor change. /// protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); if (_htmlContainer != null) _htmlContainer.HandleMouseLeave(this); } /// /// Handle mouse down to handle selection. /// protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (_htmlContainer != null) _htmlContainer.HandleMouseDown(this, e); } /// /// Handle mouse up to handle selection and link click. /// protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseClick(e); if (_htmlContainer != null) _htmlContainer.HandleMouseUp(this, e); } /// /// Handle mouse double click to select word under the mouse. /// protected override void OnMouseDoubleClick(MouseEventArgs e) { base.OnMouseDoubleClick(e); if (_htmlContainer != null) _htmlContainer.HandleMouseDoubleClick(this, e); } /// /// Handle key down event for selection, copy and scrollbars handling. /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (_htmlContainer != null) _htmlContainer.HandleKeyDown(this, e); if (e.KeyCode == Keys.Up) { VerticalScroll.Value = Math.Max(VerticalScroll.Value - 70, VerticalScroll.Minimum); PerformLayout(); } else if (e.KeyCode == Keys.Down) { VerticalScroll.Value = Math.Min(VerticalScroll.Value + 70, VerticalScroll.Maximum); PerformLayout(); } else if (e.KeyCode == Keys.PageDown) { VerticalScroll.Value = Math.Min(VerticalScroll.Value + 400, VerticalScroll.Maximum); PerformLayout(); } else if (e.KeyCode == Keys.PageUp) { VerticalScroll.Value = Math.Max(VerticalScroll.Value - 400, VerticalScroll.Minimum); PerformLayout(); } else if (e.KeyCode == Keys.End) { VerticalScroll.Value = VerticalScroll.Maximum; PerformLayout(); } else if (e.KeyCode == Keys.Home) { VerticalScroll.Value = VerticalScroll.Minimum; PerformLayout(); } } /// /// Raises the event. /// protected virtual void OnBorderStyleChanged(EventArgs e) { UpdateStyles(); var handler = BorderStyleChanged; if (handler != null) { handler(this, e); } } /// /// Propagate the LinkClicked event from root container. /// protected virtual void OnLinkClicked(HtmlLinkClickedEventArgs e) { var handler = LinkClicked; if (handler != null) handler(this, e); } /// /// 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); } /// /// Handle html renderer invalidate and re-layout as requested. /// protected virtual void OnRefresh(HtmlRefreshEventArgs e) { if (e.Layout) PerformLayout(); Invalidate(); } /// /// On html renderer scroll request adjust the scrolling of the panel to the requested location. /// protected virtual void OnScrollChange(HtmlScrollEventArgs e) { UpdateScroll(new Point((int)e.X, (int)e.Y)); } /// /// Adjust the scrolling of the panel to the requested location. /// /// the location to adjust the scroll to protected virtual void UpdateScroll(Point location) { AutoScrollPosition = location; _htmlContainer.ScrollOffset = AutoScrollPosition; } /// /// call mouse move to handle paint after scroll or html change affecting mouse cursor. /// protected virtual void InvokeMouseMove() { var mp = PointToClient(MousePosition); _htmlContainer.HandleMouseMove(this, new MouseEventArgs(MouseButtons.None, 0, mp.X, mp.Y, 0)); } /// /// Used to add arrow keys to the handled keys in . /// protected override bool IsInputKey(Keys keyData) { switch (keyData) { case Keys.Right: case Keys.Left: case Keys.Up: case Keys.Down: return true; case Keys.Shift | Keys.Right: case Keys.Shift | Keys.Left: case Keys.Shift | Keys.Up: case Keys.Shift | Keys.Down: return true; } return base.IsInputKey(keyData); } #if !MONO /// /// Override the proc processing method to set OS specific hand cursor. /// /// The Windows to process. [DebuggerStepThrough] protected override void WndProc(ref Message m) { if (_useSystemCursors && m.Msg == Win32Utils.WmSetCursor && Cursor == Cursors.Hand) { try { // Replace .NET's hand cursor with the OS cursor Win32Utils.SetCursor(Win32Utils.LoadCursor(0, Win32Utils.IdcHand)); m.Result = IntPtr.Zero; return; } catch (Exception ex) { OnRenderError(this, new HtmlRenderErrorEventArgs(HtmlRenderErrorType.General, "Failed to set OS hand cursor", ex)); } } base.WndProc(ref m); } #endif /// /// Release the html container resources. /// protected override void Dispose(bool disposing) { if (_htmlContainer != null) { _htmlContainer.LinkClicked -= OnLinkClicked; _htmlContainer.RenderError -= OnRenderError; _htmlContainer.Refresh -= OnRefresh; _htmlContainer.ScrollChange -= OnScrollChange; _htmlContainer.StylesheetLoad -= OnStylesheetLoad; _htmlContainer.ImageLoad -= OnImageLoad; _htmlContainer.Dispose(); _htmlContainer = null; } base.Dispose(disposing); } #region Private event handlers private void OnLinkClicked(object sender, HtmlLinkClickedEventArgs e) { OnLinkClicked(e); } private void OnRenderError(object sender, HtmlRenderErrorEventArgs e) { if (InvokeRequired) Invoke(new MethodInvoker(() => OnRenderError(e))); else OnRenderError(e); } private void OnStylesheetLoad(object sender, HtmlStylesheetLoadEventArgs e) { OnStylesheetLoad(e); } private void OnImageLoad(object sender, HtmlImageLoadEventArgs e) { OnImageLoad(e); } private void OnRefresh(object sender, HtmlRefreshEventArgs e) { if (InvokeRequired) Invoke(new MethodInvoker(() => OnRefresh(e))); else OnRefresh(e); } private void OnScrollChange(object sender, HtmlScrollEventArgs e) { OnScrollChange(e); } #endregion #region Hide not relevant properties from designer /// /// Not applicable. /// [Browsable(false)] public override Font Font { get { return base.Font; } set { base.Font = value; } } /// /// Not applicable. /// [Browsable(false)] public override Color ForeColor { get { return base.ForeColor; } set { base.ForeColor = value; } } /// /// Not applicable. /// [Browsable(false)] public override bool AllowDrop { get { return base.AllowDrop; } set { base.AllowDrop = value; } } /// /// Not applicable. /// [Browsable(false)] public override RightToLeft RightToLeft { get { return base.RightToLeft; } set { base.RightToLeft = value; } } /// /// Not applicable. /// [Browsable(false)] public override Cursor Cursor { get { return base.Cursor; } set { base.Cursor = value; } } /// /// Not applicable. /// [Browsable(false)] public new bool UseWaitCursor { get { return base.UseWaitCursor; } set { base.UseWaitCursor = value; } } #endregion #endregion } }