diff --git a/Source/HtmlRenderer.WinForms/Adapters/GraphicsAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/GraphicsAdapter.cs
index 6b55d50f2..45b417486 100644
--- a/Source/HtmlRenderer.WinForms/Adapters/GraphicsAdapter.cs
+++ b/Source/HtmlRenderer.WinForms/Adapters/GraphicsAdapter.cs
@@ -62,6 +62,12 @@ internal sealed class GraphicsAdapter : RGraphics
///
private readonly bool _useGdiPlusTextRendering;
+ ///
+ /// True when has a non-identity world transform (e.g. zoom ScaleTransform).
+ /// GDI TextOut ignores the GDI+ world transform, so drawing must use GDI+ in that case.
+ ///
+ private readonly bool _hasWorldTransform;
+
///
/// the initialized HDC used
///
@@ -105,7 +111,13 @@ public GraphicsAdapter(Graphics g, bool useGdiPlusTextRendering, bool releaseGra
_g = g;
_releaseGraphics = releaseGraphics;
- _useGdiPlusTextRendering = useGdiPlusTextRendering;
+ // When a world transform is active (HtmlPanel zoom), force GDI+ for DrawString so text scales.
+ // Layout measurement still prefers GDI when requested (transform is identity on layout Graphics).
+ using (var transform = g.Transform)
+ {
+ _hasWorldTransform = !transform.IsIdentity;
+ }
+ _useGdiPlusTextRendering = useGdiPlusTextRendering || _hasWorldTransform;
}
public override void PopClip()
@@ -148,7 +160,9 @@ public override void ReturnPreviousSmoothingMode(Object prevMode)
public override RSize MeasureString(string str, RFont font)
{
- if (_useGdiPlusTextRendering)
+ // Prefer GDI metrics when layout has no world transform, even if draw uses GDI+ for zoom.
+ // Measuring with an active ScaleTransform would inflate sizes and break layout.
+ if (_useGdiPlusTextRendering && !_hasWorldTransform)
{
ReleaseHdc();
var fontAdapter = (FontAdapter)font;
@@ -187,7 +201,7 @@ public override void MeasureString(string str, RFont font, double maxWidth, out
{
charFit = 0;
charFitWidth = 0;
- if (_useGdiPlusTextRendering)
+ if (_useGdiPlusTextRendering && !_hasWorldTransform)
{
ReleaseHdc();
@@ -216,7 +230,8 @@ public override void MeasureString(string str, RFont font, double maxWidth, out
public override void DrawString(string str, RFont font, RColor color, RPoint point, RSize size, bool rtl)
{
- if (_useGdiPlusTextRendering)
+ // GDI TextOut ignores Graphics.ScaleTransform; use GDI+ whenever a world transform is active (zoom).
+ if (_useGdiPlusTextRendering || _hasWorldTransform)
{
ReleaseHdc();
SetRtlAlignGdiPlus(rtl);
diff --git a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs
index 16bc83db8..cfde0294f 100644
--- a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs
+++ b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs
@@ -88,7 +88,7 @@ protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, R
return new BrushAdapter(new LinearGradientBrush(Utils.Convert(rect), Utils.Convert(color1), Utils.Convert(color2), (float)angle), true);
}
- protected override RImage ConvertImageInt(object image)
+ protected override RImage? ConvertImageInt(object? image)
{
return image != null ? new ImageAdapter((Image)image) : null;
}
diff --git a/Source/HtmlRenderer.WinForms/HtmlContainer.cs b/Source/HtmlRenderer.WinForms/HtmlContainer.cs
index 5bc7c5cd0..ee76bacbd 100644
--- a/Source/HtmlRenderer.WinForms/HtmlContainer.cs
+++ b/Source/HtmlRenderer.WinForms/HtmlContainer.cs
@@ -286,7 +286,7 @@ public SizeF ActualSize
///
/// Get the currently selected text segment in the html.
///
- public string SelectedText
+ public string? SelectedText
{
get { return _htmlContainerInt.SelectedText; }
}
@@ -294,7 +294,7 @@ public string SelectedText
///
/// Copy the currently selected html segment with style.
///
- public string SelectedHtml
+ public string? SelectedHtml
{
get { return _htmlContainerInt.SelectedHtml; }
}
@@ -312,7 +312,7 @@ public void ClearSelection()
///
/// the html to init with, init empty if not given
/// optional: the stylesheet to init with, init default if not given
- public void SetHtml(string htmlSource, CssData baseCssData = null)
+ public void SetHtml(string? htmlSource, CssData? baseCssData = null)
{
_htmlContainerInt.SetHtml(htmlSource, baseCssData);
}
@@ -322,7 +322,7 @@ public void SetHtml(string htmlSource, CssData baseCssData = null)
///
/// Optional: controls the way styles are generated when html is generated (default: )
/// generated html
- public string GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline)
+ public string? GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline)
{
return _htmlContainerInt.GetHtml(styleGen);
}
@@ -334,7 +334,7 @@ public string GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline)
/// the location to find the attribute at
/// the attribute key to get value by
/// found attribute value or null if not found
- public string GetAttributeAt(Point location, string attribute)
+ public string? GetAttributeAt(Point location, string attribute)
{
return _htmlContainerInt.GetAttributeAt(Utils.Convert(location), attribute);
}
@@ -358,7 +358,7 @@ public List> GetLinks()
///
/// the location to find the link at
/// css link href if exists or null
- public string GetLinkAt(Point location)
+ public string? GetLinkAt(Point location)
{
return _htmlContainerInt.GetLinkAt(Utils.Convert(location));
}
diff --git a/Source/HtmlRenderer.WinForms/HtmlLabel.cs b/Source/HtmlRenderer.WinForms/HtmlLabel.cs
index 283ef7360..645d418bc 100644
--- a/Source/HtmlRenderer.WinForms/HtmlLabel.cs
+++ b/Source/HtmlRenderer.WinForms/HtmlLabel.cs
@@ -75,7 +75,7 @@ public class HtmlLabel : Control
///
/// Underline html container instance.
///
- protected HtmlContainer _htmlContainer;
+ protected HtmlContainer? _htmlContainer;
///
/// The current border style of the control
@@ -85,17 +85,17 @@ public class HtmlLabel : Control
///
/// the raw base stylesheet data used in the control
///
- protected string _baseRawCssData;
+ protected string? _baseRawCssData;
///
/// the base stylesheet data used in the panel
///
- protected CssData _baseCssData;
+ protected CssData? _baseCssData;
///
/// the current html text set in the control
///
- protected string _text;
+ protected string? _text;
///
/// is to handle auto size of the control height only
@@ -296,7 +296,7 @@ public virtual bool IsContextMenuEnabled
[Description("Set base stylesheet to be used by html rendered in the control.")]
[Category("Appearance")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
- [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")]
+ [Editor("System.ComponentModel.Design.MultilineStringEditor, System.Windows.Forms.Design, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Drawing.Design.UITypeEditor, System.Windows.Forms, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public virtual string BaseStylesheet
{
get { return _baseRawCssData; }
@@ -488,21 +488,17 @@ protected override void OnLayout(LayoutEventArgs levent)
{
if (_htmlContainer != null)
{
- Graphics g = CreateGraphics();
- if (g != null)
+ using (var g = CreateGraphics())
+ using (var ig = new GraphicsAdapter(g, _htmlContainer.UseGdiPlusTextRendering))
{
- using (g)
- using (var ig = new GraphicsAdapter(g, _htmlContainer.UseGdiPlusTextRendering))
- {
- var newSize = HtmlRendererUtils.Layout(ig,
+ var newSize = HtmlRendererUtils.Layout(ig,
_htmlContainer.HtmlContainerInt,
new RSize(ClientSize.Width - Padding.Horizontal, ClientSize.Height - Padding.Vertical),
new RSize(MinimumSize.Width - Padding.Horizontal, MinimumSize.Height - Padding.Vertical),
new RSize(MaximumSize.Width - Padding.Horizontal, MaximumSize.Height - Padding.Vertical),
AutoSize,
AutoSizeHeightOnly);
- ClientSize = Utils.ConvertRound(new RSize(newSize.Width + Padding.Horizontal, newSize.Height + Padding.Vertical));
- }
+ ClientSize = Utils.ConvertRound(new RSize(newSize.Width + Padding.Horizontal, newSize.Height + Padding.Vertical));
}
}
base.OnLayout(levent);
@@ -659,7 +655,7 @@ protected override void WndProc(ref Message m)
try
{
// Replace .NET's hand cursor with the OS cursor
- Win32Utils.SetCursor(Win32Utils.LoadCursor(0, Win32Utils.IdcHand));
+ Win32Utils.SetCursor(Win32Utils.LoadCursor(IntPtr.Zero, Win32Utils.IdcHand));
m.Result = IntPtr.Zero;
return;
}
diff --git a/Source/HtmlRenderer.WinForms/HtmlPanel.cs b/Source/HtmlRenderer.WinForms/HtmlPanel.cs
index a2f061874..112b8415e 100644
--- a/Source/HtmlRenderer.WinForms/HtmlPanel.cs
+++ b/Source/HtmlRenderer.WinForms/HtmlPanel.cs
@@ -59,6 +59,12 @@ namespace TheArtOfDev.HtmlRenderer.WinForms
/// RenderError event:
/// Raised when an error occurred during html rendering.
///
+ ///
+ /// Zoom and pan:
+ /// Ctrl+MouseWheel zooms the content with anchor-to-cursor behavior (also Ctrl+/− and Ctrl+0).
+ /// Middle-mouse button drag pans the canvas via scroll position.
+ /// See for programmatic control.
+ ///
///
public class HtmlPanel : ScrollableControl
{
@@ -77,17 +83,17 @@ public class HtmlPanel : ScrollableControl
///
/// the raw base stylesheet data used in the control
///
- protected string _baseRawCssData;
+ protected string? _baseRawCssData;
///
/// the base stylesheet data used in the control
///
- protected CssData _baseCssData;
+ protected CssData? _baseCssData;
///
/// the current html text set in the control
///
- protected string _text;
+ protected string? _text;
///
/// If to use cursors defined by the operating system or .NET cursors
@@ -104,6 +110,41 @@ public class HtmlPanel : ScrollableControl
///
protected Point _lastScrollOffset;
+ ///
+ /// The zoom factor of the rendered HTML (1.0 = 100%).
+ ///
+ protected float _zoom = 1f;
+
+ ///
+ /// True while middle-mouse button pan is active.
+ ///
+ private bool _isPanning;
+
+ ///
+ /// Last client mouse position during middle-mouse pan.
+ ///
+ private Point _panLastPoint;
+
+ ///
+ /// Cursor to restore when middle-mouse pan ends.
+ ///
+ private Cursor? _cursorBeforePan;
+
+ ///
+ /// Minimum allowed zoom factor.
+ ///
+ private const float MinZoom = 0.25f;
+
+ ///
+ /// Maximum allowed zoom factor.
+ ///
+ private const float MaxZoom = 5f;
+
+ ///
+ /// Zoom change per Ctrl+wheel / Ctrl+/- step.
+ ///
+ private const float ZoomStep = 0.1f;
+
#endregion
@@ -134,6 +175,12 @@ public HtmlPanel()
[Category("Property Changed")]
public event EventHandler BorderStyleChanged;
+ ///
+ /// Raised when the property value changes.
+ ///
+ [Category("Property Changed")]
+ public event EventHandler ZoomChanged;
+
///
/// Raised when the set html document has been fully loaded.
/// Allows manipulation of the html dom, scroll position, etc.
@@ -247,6 +294,19 @@ public bool UseSystemCursors
set { _useSystemCursors = value; }
}
+ ///
+ /// Gets or sets the zoom factor of the rendered HTML (1.0 = 100%).
+ /// Values are clamped to the range 0.25–5.0. Setting this property anchors zoom to the viewport center.
+ ///
+ [Category("Behavior")]
+ [DefaultValue(1f)]
+ [Description("The zoom factor of the rendered HTML (1.0 = 100%).")]
+ public virtual float Zoom
+ {
+ get { return _zoom; }
+ set { SetZoom(value, null); }
+ }
+
///
/// Gets or sets the border style.
///
@@ -304,7 +364,7 @@ public virtual bool IsContextMenuEnabled
[Category("Appearance")]
[Description("Set base stylesheet to be used by html rendered in the control.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
- [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")]
+ [Editor("System.ComponentModel.Design.MultilineStringEditor, System.Windows.Forms.Design, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Drawing.Design.UITypeEditor, System.Windows.Forms, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public virtual string BaseStylesheet
{
get { return _baseRawCssData; }
@@ -404,8 +464,9 @@ public virtual void ScrollToElement(string elementId)
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));
+ var location = rect.Value.Location;
+ UpdateScroll(Point.Round(new PointF(location.X * _zoom, location.Y * _zoom)));
+ InvokeMouseMove();
}
}
}
@@ -419,6 +480,55 @@ public void ClearSelection()
_htmlContainer.ClearSelection();
}
+ ///
+ /// Set the zoom factor, optionally anchoring the scroll so the document point under
+ /// stays fixed on screen (browser-style zoom).
+ ///
+ /// desired zoom factor (clamped to 0.25–5.0)
+ /// client-space anchor; null uses the viewport center
+ public virtual void SetZoom(float zoom, Point? anchorClientPoint)
+ {
+ float z1 = Math.Max(MinZoom, Math.Min(MaxZoom, zoom));
+ if (Math.Abs(z1 - _zoom) < 0.0001f)
+ return;
+
+ float z0 = _zoom;
+ Point anchor = anchorClientPoint ?? new Point(ClientSize.Width / 2, ClientSize.Height / 2);
+
+ // AutoScrollPosition is negative when scrolled; convert anchor to logical document coords
+ double docX = (anchor.X - AutoScrollPosition.X) / z0;
+ double docY = (anchor.Y - AutoScrollPosition.Y) / z0;
+
+ _zoom = z1;
+ PerformLayout();
+
+ // Keep the same document point under the anchor after zoom
+ int scrollX = (int)Math.Round(docX * z1 - anchor.X);
+ int scrollY = (int)Math.Round(docY * z1 - anchor.Y);
+ scrollX = ClampScroll(scrollX, AutoScrollMinSize.Width, ClientSize.Width);
+ scrollY = ClampScroll(scrollY, AutoScrollMinSize.Height, ClientSize.Height);
+ AutoScrollPosition = new Point(scrollX, scrollY);
+ SyncHtmlScrollOffset();
+ Invalidate();
+ OnZoomChanged(EventArgs.Empty);
+ }
+
+ ///
+ /// Increase zoom by one step, anchored at the given client point (or viewport center).
+ ///
+ public void ZoomIn(Point? anchorClientPoint = null)
+ {
+ SetZoom(_zoom + ZoomStep, anchorClientPoint);
+ }
+
+ ///
+ /// Decrease zoom by one step, anchored at the given client point (or viewport center).
+ ///
+ public void ZoomOut(Point? anchorClientPoint = null)
+ {
+ SetZoom(_zoom - ZoomStep, anchorClientPoint);
+ }
+
#region Private methods
///
@@ -455,7 +565,8 @@ protected override void OnLayout(LayoutEventArgs levent)
base.OnLayout(levent);
// to handle if vertical scrollbar is appearing or disappearing
- if (_htmlContainer != null && Math.Abs(_htmlContainer.MaxSize.Width - ClientSize.Width) > 0.1)
+ float layoutWidth = (ClientSize.Width - Padding.Horizontal) / _zoom;
+ if (_htmlContainer != null && Math.Abs(_htmlContainer.MaxSize.Width - layoutWidth) > 0.1)
{
PerformHtmlLayout();
base.OnLayout(levent);
@@ -463,24 +574,22 @@ protected override void OnLayout(LayoutEventArgs levent)
}
///
- /// Perform html container layout by the current panel client size.
+ /// Perform html container layout by the current panel client size and zoom.
///
protected void PerformHtmlLayout()
{
if (_htmlContainer != null)
{
- _htmlContainer.MaxSize = new SizeF(ClientSize.Width - Padding.Horizontal, 0);
+ _htmlContainer.MaxSize = new SizeF((ClientSize.Width - Padding.Horizontal) / _zoom, 0);
- Graphics g = CreateGraphics();
- if (g != null)
+ using (var g = CreateGraphics())
{
- using (g)
- {
- _htmlContainer.PerformLayout(g);
- }
+ _htmlContainer.PerformLayout(g);
}
- AutoScrollMinSize = Size.Round(new SizeF(_htmlContainer.ActualSize.Width + Padding.Horizontal, _htmlContainer.ActualSize.Height));
+ AutoScrollMinSize = Size.Round(new SizeF(
+ (_htmlContainer.ActualSize.Width + Padding.Horizontal) * _zoom,
+ _htmlContainer.ActualSize.Height * _zoom));
}
}
@@ -495,8 +604,9 @@ protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.TextRenderingHint = _textRenderingHint;
e.Graphics.SetClip(ClientRectangle);
- _htmlContainer.Location = new PointF(Padding.Left, Padding.Top);
- _htmlContainer.ScrollOffset = AutoScrollPosition;
+ e.Graphics.ScaleTransform(_zoom, _zoom);
+ _htmlContainer.Location = new PointF(Padding.Left / _zoom, Padding.Top / _zoom);
+ SyncHtmlScrollOffset();
_htmlContainer.PerformPaint(e.Graphics);
if (!_lastScrollOffset.Equals(_htmlContainer.ScrollOffset))
@@ -517,13 +627,28 @@ protected override void OnClick(EventArgs e)
}
///
- /// Handle mouse move to handle hover cursor and text selection.
+ /// Handle mouse move for middle-mouse pan, hover cursor and text selection.
///
protected override void OnMouseMove(MouseEventArgs e)
{
+ if (_isPanning)
+ {
+ int dx = e.X - _panLastPoint.X;
+ int dy = e.Y - _panLastPoint.Y;
+ int scrollX = -AutoScrollPosition.X - dx;
+ int scrollY = -AutoScrollPosition.Y - dy;
+ scrollX = ClampScroll(scrollX, AutoScrollMinSize.Width, ClientSize.Width);
+ scrollY = ClampScroll(scrollY, AutoScrollMinSize.Height, ClientSize.Height);
+ AutoScrollPosition = new Point(scrollX, scrollY);
+ SyncHtmlScrollOffset();
+ _panLastPoint = e.Location;
+ Invalidate();
+ return;
+ }
+
base.OnMouseMove(e);
if (_htmlContainer != null)
- _htmlContainer.HandleMouseMove(this, e);
+ _htmlContainer.HandleMouseMove(this, CreateHtmlMouseEventArgs(e));
}
///
@@ -532,38 +657,80 @@ protected override void OnMouseMove(MouseEventArgs e)
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
- if (_htmlContainer != null)
+ if (!_isPanning && _htmlContainer != null)
_htmlContainer.HandleMouseLeave(this);
}
///
- /// Handle mouse down to handle selection.
+ /// Handle mouse down for middle-mouse pan and selection.
///
protected override void OnMouseDown(MouseEventArgs e)
{
+ if (e.Button == MouseButtons.Middle)
+ {
+ StartPan(e.Location);
+ return;
+ }
+
base.OnMouseDown(e);
if (_htmlContainer != null)
- _htmlContainer.HandleMouseDown(this, e);
+ _htmlContainer.HandleMouseDown(this, CreateHtmlMouseEventArgs(e));
}
///
- /// Handle mouse up to handle selection and link click.
+ /// Handle mouse up for middle-mouse pan, selection and link click.
///
protected override void OnMouseUp(MouseEventArgs e)
{
- base.OnMouseUp(e);
+ if (_isPanning && e.Button == MouseButtons.Middle)
+ {
+ EndPan();
+ return;
+ }
+
+ base.OnMouseUp(e);
if (_htmlContainer != null)
- _htmlContainer.HandleMouseUp(this, e);
+ _htmlContainer.HandleMouseUp(this, CreateHtmlMouseEventArgs(e));
}
///
- /// Handle mouse double click to select word under the mouse.
+ /// 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);
+ _htmlContainer.HandleMouseDoubleClick(this, CreateHtmlMouseEventArgs(e));
+ }
+
+ ///
+ /// Ctrl+MouseWheel zooms (anchor-to-cursor); otherwise normal scroll.
+ ///
+ protected override void OnMouseWheel(MouseEventArgs e)
+ {
+ if ((ModifierKeys & Keys.Control) == Keys.Control)
+ {
+ if (e.Delta > 0)
+ ZoomIn(e.Location);
+ else if (e.Delta < 0)
+ ZoomOut(e.Location);
+
+ if (e is HandledMouseEventArgs handled)
+ handled.Handled = true;
+ return;
+ }
+
+ base.OnMouseWheel(e);
+ }
+
+ ///
+ /// End middle-mouse pan if mouse capture is lost.
+ ///
+ protected override void OnMouseCaptureChanged(EventArgs e)
+ {
+ base.OnMouseCaptureChanged(e);
+ if (_isPanning && !Capture)
+ EndPan();
}
///
@@ -606,6 +773,34 @@ protected override void OnKeyDown(KeyEventArgs e)
}
}
+ ///
+ /// Handle Ctrl+/-, Ctrl+=, and Ctrl+0 zoom shortcuts.
+ ///
+ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
+ {
+ if ((keyData & Keys.Control) == Keys.Control)
+ {
+ Keys key = keyData & ~(Keys.Control | Keys.Shift);
+ Point anchor = GetZoomAnchorFromCursor();
+ if (key == Keys.Oemplus || key == Keys.Add)
+ {
+ ZoomIn(anchor);
+ return true;
+ }
+ if (key == Keys.OemMinus || key == Keys.Subtract)
+ {
+ ZoomOut(anchor);
+ return true;
+ }
+ if (key == Keys.D0 || key == Keys.NumPad0)
+ {
+ SetZoom(1f, anchor);
+ return true;
+ }
+ }
+ return base.ProcessCmdKey(ref msg, keyData);
+ }
+
///
/// Raises the event.
///
@@ -620,6 +815,16 @@ protected virtual void OnBorderStyleChanged(EventArgs e)
}
}
+ ///
+ /// Raises the event.
+ ///
+ protected virtual void OnZoomChanged(EventArgs e)
+ {
+ var handler = ZoomChanged;
+ if (handler != null)
+ handler(this, e);
+ }
+
///
/// Propagate the LoadComplete event from root container.
///
@@ -689,13 +894,13 @@ protected virtual void OnScrollChange(HtmlScrollEventArgs e)
}
///
- /// Adjust the scrolling of the panel to the requested location.
+ /// Adjust the scrolling of the panel to the requested location (client / zoomed space).
///
/// the location to adjust the scroll to
protected virtual void UpdateScroll(Point location)
{
AutoScrollPosition = location;
- _htmlContainer.ScrollOffset = AutoScrollPosition;
+ SyncHtmlScrollOffset();
}
///
@@ -703,9 +908,12 @@ protected virtual void UpdateScroll(Point location)
///
protected virtual void InvokeMouseMove()
{
+ if (_isPanning || _htmlContainer == null)
+ return;
+
try
{
- var mp = PointToClient(MousePosition);
+ var mp = PointToHtml(PointToClient(MousePosition));
_htmlContainer.HandleMouseMove(this, new MouseEventArgs(MouseButtons.None, 0, mp.X, mp.Y, 0));
}
catch
@@ -714,6 +922,90 @@ protected virtual void InvokeMouseMove()
}
}
+ ///
+ /// Convert client-space point to HTML logical coordinates (accounts for zoom).
+ ///
+ private Point PointToHtml(Point clientPoint)
+ {
+ if (Math.Abs(_zoom - 1f) < 0.0001f)
+ return clientPoint;
+ return new Point(
+ (int)Math.Round(clientPoint.X / _zoom),
+ (int)Math.Round(clientPoint.Y / _zoom));
+ }
+
+ ///
+ /// Create mouse args with location converted to HTML logical coordinates.
+ ///
+ private MouseEventArgs CreateHtmlMouseEventArgs(MouseEventArgs e)
+ {
+ var p = PointToHtml(e.Location);
+ return new MouseEventArgs(e.Button, e.Clicks, p.X, p.Y, e.Delta);
+ }
+
+ ///
+ /// Sync html container scroll offset from AutoScrollPosition in logical units.
+ ///
+ private void SyncHtmlScrollOffset()
+ {
+ if (_htmlContainer == null)
+ return;
+ var sp = AutoScrollPosition;
+ _htmlContainer.ScrollOffset = new Point(
+ (int)Math.Round(sp.X / _zoom),
+ (int)Math.Round(sp.Y / _zoom));
+ }
+
+ ///
+ /// Clamp a positive scroll offset to the valid range for the current content size.
+ ///
+ private static int ClampScroll(int offset, int contentSize, int viewportSize)
+ {
+ int max = Math.Max(0, contentSize - viewportSize);
+ if (offset < 0)
+ return 0;
+ if (offset > max)
+ return max;
+ return offset;
+ }
+
+ ///
+ /// Start middle-mouse pan at the given client point.
+ ///
+ private void StartPan(Point clientPoint)
+ {
+ _isPanning = true;
+ _panLastPoint = clientPoint;
+ _cursorBeforePan = Cursor;
+ Cursor = Cursors.SizeAll;
+ Capture = true;
+ Focus();
+ }
+
+ ///
+ /// End middle-mouse pan and restore cursor/capture.
+ ///
+ private void EndPan()
+ {
+ if (!_isPanning)
+ return;
+ _isPanning = false;
+ Capture = false;
+ Cursor = _cursorBeforePan ?? Cursors.Default;
+ InvokeMouseMove();
+ }
+
+ ///
+ /// Client point under the cursor if it is over this control; otherwise viewport center.
+ ///
+ private Point GetZoomAnchorFromCursor()
+ {
+ var client = PointToClient(MousePosition);
+ if (ClientRectangle.Contains(client))
+ return client;
+ return new Point(ClientSize.Width / 2, ClientSize.Height / 2);
+ }
+
///
/// Used to add arrow keys to the handled keys in .
///
@@ -747,7 +1039,7 @@ protected override void WndProc(ref Message m)
try
{
// Replace .NET's hand cursor with the OS cursor
- Win32Utils.SetCursor(Win32Utils.LoadCursor(0, Win32Utils.IdcHand));
+ Win32Utils.SetCursor(Win32Utils.LoadCursor(IntPtr.Zero, Win32Utils.IdcHand));
m.Result = IntPtr.Zero;
return;
}
diff --git a/Source/HtmlRenderer.WinForms/HtmlRenderer.WinForms.csproj b/Source/HtmlRenderer.WinForms/HtmlRenderer.WinForms.csproj
index 51aec7512..d05d042ee 100644
--- a/Source/HtmlRenderer.WinForms/HtmlRenderer.WinForms.csproj
+++ b/Source/HtmlRenderer.WinForms/HtmlRenderer.WinForms.csproj
@@ -1,11 +1,11 @@
- net8.0-windows
+ net10.0-windows
Library
TheArtOfDev.HtmlRenderer.WinForms
true
+ enable
true
- true
diff --git a/Source/HtmlRenderer.WinForms/HtmlToolTip.cs b/Source/HtmlRenderer.WinForms/HtmlToolTip.cs
index 2a82209bd..adb087822 100644
--- a/Source/HtmlRenderer.WinForms/HtmlToolTip.cs
+++ b/Source/HtmlRenderer.WinForms/HtmlToolTip.cs
@@ -31,17 +31,17 @@ public class HtmlToolTip : ToolTip
///
/// the container to render and handle the html shown in the tooltip
///
- protected HtmlContainer _htmlContainer;
+ protected HtmlContainer? _htmlContainer;
///
/// the raw base stylesheet data used in the control
///
- protected string _baseRawCssData;
+ protected string? _baseRawCssData;
///
/// the base stylesheet data used in the panel
///
- protected CssData _baseCssData;
+ protected CssData? _baseCssData;
///
/// The text rendering hint to be used for text rendering.
@@ -57,13 +57,13 @@ public class HtmlToolTip : ToolTip
/// the control that the tooltip is currently showing on.
/// Used for link handling.
///
- private Control _associatedControl;
+ private Control? _associatedControl;
///
/// timer used to handle mouse move events when mouse is over the tooltip.
/// Used for link handling.
///
- private Timer _linkHandlingTimer;
+ private Timer? _linkHandlingTimer;
///
/// the handle of the actual tooltip window used to know when the tooltip is hidden
@@ -175,7 +175,7 @@ public TextRenderingHint TextRenderingHint
[Description("Set base stylesheet to be used by html rendered in the tooltip.")]
[Category("Appearance")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
- [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")]
+ [Editor("System.ComponentModel.Design.MultilineStringEditor, System.Windows.Forms.Design, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Drawing.Design.UITypeEditor, System.Windows.Forms, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public virtual string BaseStylesheet
{
get { return _baseRawCssData; }
@@ -238,6 +238,9 @@ public virtual Size MaximumSize
///
protected virtual void OnToolTipPopup(PopupEventArgs e)
{
+ if (e.AssociatedControl == null)
+ return;
+
//Create fragment container
var cssClass = string.IsNullOrEmpty(_tooltipCssClass) ? null : string.Format(" class=\"{0}\"", _tooltipCssClass);
var toolipHtml = string.Format("{1}
", cssClass, GetToolTip(e.AssociatedControl));
@@ -276,7 +279,8 @@ protected virtual void OnToolTipDraw(DrawToolTipEventArgs e)
_tooltipHandle = Win32Utils.WindowFromDC(hdc);
e.Graphics.ReleaseHdc(hdc);
- AdjustTooltipPosition(e.AssociatedControl, e.Bounds.Size);
+ if (e.AssociatedControl != null)
+ AdjustTooltipPosition(e.AssociatedControl, e.Bounds.Size);
}
e.Graphics.Clear(Color.White);
@@ -373,16 +377,19 @@ protected virtual void OnLinkHandlingTimerTick(EventArgs e)
_linkHandlingTimer.Stop();
_tooltipHandle = IntPtr.Zero;
- var mPos = Control.MousePosition;
- var mButtons = Control.MouseButtons;
- var rect = Win32Utils.GetWindowRectangle(handle);
- if (rect.Contains(mPos))
+ if (handle != IntPtr.Zero && _associatedControl != null)
{
- if (mButtons == MouseButtons.Left)
+ var mPos = Control.MousePosition;
+ var mButtons = Control.MouseButtons;
+ var rect = Win32Utils.GetWindowRectangle(handle);
+ if (rect.Contains(mPos))
{
- var args = new MouseEventArgs(mButtons, 1, mPos.X - rect.X, mPos.Y - rect.Y, 0);
- _htmlContainer.HandleMouseDown(_associatedControl, args);
- _htmlContainer.HandleMouseUp(_associatedControl, args);
+ 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);
+ }
}
}
}
@@ -402,23 +409,22 @@ protected virtual void OnToolTipDisposed(EventArgs e)
Draw -= OnToolTipDraw;
Disposed -= OnToolTipDisposed;
+ if (_linkHandlingTimer != null)
+ {
+ _linkHandlingTimer.Tick -= OnLinkHandlingTimerTick;
+ _linkHandlingTimer.Dispose();
+ _linkHandlingTimer = null;
+ }
+
if (_htmlContainer != null)
{
+ _htmlContainer.LinkClicked -= OnLinkClicked;
_htmlContainer.RenderError -= OnRenderError;
_htmlContainer.StylesheetLoad -= OnStylesheetLoad;
_htmlContainer.ImageLoad -= OnImageLoad;
_htmlContainer.Dispose();
_htmlContainer = null;
}
-
- if (_linkHandlingTimer != null)
- {
- _linkHandlingTimer.Dispose();
- _linkHandlingTimer = null;
-
- if (_htmlContainer != null)
- _htmlContainer.LinkClicked -= OnLinkClicked;
- }
}
diff --git a/Source/HtmlRenderer.WinForms/MetafileExtensions.cs b/Source/HtmlRenderer.WinForms/MetafileExtensions.cs
index 7bc5fad25..73aad459f 100644
--- a/Source/HtmlRenderer.WinForms/MetafileExtensions.cs
+++ b/Source/HtmlRenderer.WinForms/MetafileExtensions.cs
@@ -14,24 +14,25 @@ public static void SaveAsEmf(Metafile me, string fileName)
by : SWAT Team member _1
Date : Friday, February 01, 2008 1:38 PM
*/
- int enfMetafileHandle = me.GetHenhmetafile().ToInt32();
+ IntPtr enfMetafileHandle = me.GetHenhmetafile();
int bufferSize = GetEnhMetaFileBits(enfMetafileHandle, 0, null); // Get required buffer size.
byte[] buffer = new byte[bufferSize]; // Allocate sufficient buffer
if (GetEnhMetaFileBits(enfMetafileHandle, bufferSize, buffer) <= 0) // Get raw metafile data.
throw new SystemException("Fail");
- FileStream ms = File.Open(fileName, FileMode.Create);
- ms.Write(buffer, 0, bufferSize);
- ms.Close();
- ms.Dispose();
+ using (var ms = File.Open(fileName, FileMode.Create))
+ {
+ ms.Write(buffer, 0, bufferSize);
+ }
+
if (!DeleteEnhMetaFile(enfMetafileHandle)) //free handle
throw new SystemException("Fail Free");
}
- [DllImport("gdi32")]
- public static extern int GetEnhMetaFileBits(int hemf, int cbBuffer, byte[] lpbBuffer);
+ [DllImport("gdi32.dll")]
+ public static extern int GetEnhMetaFileBits(IntPtr hemf, int cbBuffer, byte[] lpbBuffer);
- [DllImport("gdi32")]
- public static extern bool DeleteEnhMetaFile(int hemfbitHandle);
+ [DllImport("gdi32.dll")]
+ public static extern bool DeleteEnhMetaFile(IntPtr hemfbitHandle);
}
}
diff --git a/Source/HtmlRenderer.WinForms/Utilities/Utils.cs b/Source/HtmlRenderer.WinForms/Utilities/Utils.cs
index 026102976..61a2ce0f5 100644
--- a/Source/HtmlRenderer.WinForms/Utilities/Utils.cs
+++ b/Source/HtmlRenderer.WinForms/Utilities/Utils.cs
@@ -13,6 +13,7 @@
using System;
using System.Drawing;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
namespace TheArtOfDev.HtmlRenderer.WinForms.Utilities
{
@@ -34,6 +35,7 @@ public static RPoint Convert(PointF p)
///
public static PointF[] Convert(RPoint[] points)
{
+ ArgChecker.AssertArgNotNull(points, nameof(points));
PointF[] myPoints = new PointF[points.Length];
for (int i = 0; i < points.Length; i++)
myPoints[i] = Convert(points[i]);
diff --git a/Source/HtmlRenderer.WinForms/Utilities/Win32Utils.cs b/Source/HtmlRenderer.WinForms/Utilities/Win32Utils.cs
index 228e47658..7db9fdbf0 100644
--- a/Source/HtmlRenderer.WinForms/Utilities/Win32Utils.cs
+++ b/Source/HtmlRenderer.WinForms/Utilities/Win32Utils.cs
@@ -47,11 +47,11 @@ internal static class Win32Utils
public const int TextAlignBaselineRtl = 256 + 24;
- [DllImport("user32.dll")]
- public static extern int SetCursor(int hCursor);
+ [DllImport("user32.dll", SetLastError = true)]
+ public static extern IntPtr SetCursor(IntPtr hCursor);
- [DllImport("user32.dll")]
- public static extern int LoadCursor(int hInstance, int lpCursorName);
+ [DllImport("user32.dll", SetLastError = true)]
+ public static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName);
///
/// Create a compatible memory HDC from the given HDC.
diff --git a/Source/HtmlRenderer/Adapters/Entities/RColor.cs b/Source/HtmlRenderer/Adapters/Entities/RColor.cs
index 83fb26a12..8330a97f5 100644
--- a/Source/HtmlRenderer/Adapters/Entities/RColor.cs
+++ b/Source/HtmlRenderer/Adapters/Entities/RColor.cs
@@ -211,7 +211,7 @@ public static RColor FromArgb(int red, int green, int blue)
///
/// The object to test.
/// 1
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (obj is RColor)
{
diff --git a/Source/HtmlRenderer/Adapters/Entities/RPoint.cs b/Source/HtmlRenderer/Adapters/Entities/RPoint.cs
index eb36c8b47..2dedde7dd 100644
--- a/Source/HtmlRenderer/Adapters/Entities/RPoint.cs
+++ b/Source/HtmlRenderer/Adapters/Entities/RPoint.cs
@@ -251,7 +251,7 @@ public static RPoint Subtract(RPoint pt, RSize sz)
/// The to test.
///
/// 1
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (!(obj is RPoint))
return false;
diff --git a/Source/HtmlRenderer/Adapters/Entities/RRect.cs b/Source/HtmlRenderer/Adapters/Entities/RRect.cs
index 3ae148655..f3bd10f3e 100644
--- a/Source/HtmlRenderer/Adapters/Entities/RRect.cs
+++ b/Source/HtmlRenderer/Adapters/Entities/RRect.cs
@@ -284,7 +284,7 @@ public static RRect FromLTRB(double left, double top, double right, double botto
///
/// The to test.
///
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (!(obj is RRect))
return false;
diff --git a/Source/HtmlRenderer/Adapters/Entities/RSize.cs b/Source/HtmlRenderer/Adapters/Entities/RSize.cs
index 521bce308..903738492 100644
--- a/Source/HtmlRenderer/Adapters/Entities/RSize.cs
+++ b/Source/HtmlRenderer/Adapters/Entities/RSize.cs
@@ -287,7 +287,7 @@ public static RSize Subtract(RSize sz1, RSize sz2)
/// The to test.
///
/// 1
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (!(obj is RSize))
return false;
diff --git a/Source/HtmlRenderer/Adapters/RAdapter.cs b/Source/HtmlRenderer/Adapters/RAdapter.cs
index 6b13459c1..85ad72a76 100644
--- a/Source/HtmlRenderer/Adapters/RAdapter.cs
+++ b/Source/HtmlRenderer/Adapters/RAdapter.cs
@@ -365,7 +365,7 @@ internal RFont CreateFont(RFontFamily family, double size, RFontStyle style)
///
/// the image returned from load event
/// converted image or null
- protected abstract RImage ConvertImageInt(object image);
+ protected abstract RImage? ConvertImageInt(object? image);
///
/// Create an object from the given stream.
diff --git a/Source/HtmlRenderer/Core/Entities/CssBlock.cs b/Source/HtmlRenderer/Core/Entities/CssBlock.cs
index 2982b3159..fbcaca88e 100644
--- a/Source/HtmlRenderer/Core/Entities/CssBlock.cs
+++ b/Source/HtmlRenderer/Core/Entities/CssBlock.cs
@@ -196,7 +196,7 @@ public bool EqualsSelector(CssBlock other)
///
/// the other block to compare to
/// true - the two blocks are the same, false - otherwise
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;
diff --git a/Source/HtmlRenderer/Core/Handlers/ImageDownloader.cs b/Source/HtmlRenderer/Core/Handlers/ImageDownloader.cs
index 2f8b0f5e6..3d6b97d5b 100644
--- a/Source/HtmlRenderer/Core/Handlers/ImageDownloader.cs
+++ b/Source/HtmlRenderer/Core/Handlers/ImageDownloader.cs
@@ -39,10 +39,15 @@ namespace TheArtOfDev.HtmlRenderer.Core.Handlers
internal sealed class ImageDownloader : IDisposable
{
///
- /// the web client used to download image from URL (to cancel on dispose)
+ /// List of web clients used to download images (to allow cancel on dispose).
///
private readonly List _clients = new List();
+ ///
+ /// Lock object for thread-safe _clients access.
+ ///
+ private readonly object _clientsLock = new object();
+
///
/// dictionary of image cache path to callbacks of download to handle multiple requests to download the same image
///
@@ -111,7 +116,7 @@ private void DownloadImageFromUrl(Uri source, string tempPath, string filePath)
{
using (var client = new WebClient())
{
- _clients.Add(client);
+ lock (_clientsLock) { _clients.Add(client); }
client.DownloadFile(source, tempPath);
OnDownloadImageCompleted(client, source, tempPath, filePath, null, false);
}
@@ -133,7 +138,7 @@ private void DownloadImageFromUrlAsync(object data)
try
{
var client = new WebClient();
- _clients.Add(client);
+ lock (_clientsLock) { _clients.Add(client); }
client.DownloadFileCompleted += OnDownloadImageAsyncCompleted;
client.DownloadFileAsync(downloadData._uri, downloadData._tempPath, downloadData);
}
@@ -226,17 +231,20 @@ private void OnDownloadImageCompleted(WebClient client, Uri source, string tempP
private void ReleaseObjects()
{
_imageDownloadCallbacks.Clear();
- while (_clients.Count > 0)
+ lock (_clientsLock)
{
- try
+ while (_clients.Count > 0)
{
- var client = _clients[0];
- client.CancelAsync();
- client.Dispose();
- _clients.RemoveAt(0);
+ try
+ {
+ var client = _clients[0];
+ client.CancelAsync();
+ client.Dispose();
+ _clients.RemoveAt(0);
+ }
+ catch
+ { }
}
- catch
- { }
}
}
diff --git a/Source/HtmlRenderer/Core/Handlers/ImageLoadHandler.cs b/Source/HtmlRenderer/Core/Handlers/ImageLoadHandler.cs
index eb9ace4fd..5acaa7af8 100644
--- a/Source/HtmlRenderer/Core/Handlers/ImageLoadHandler.cs
+++ b/Source/HtmlRenderer/Core/Handlers/ImageLoadHandler.cs
@@ -54,6 +54,11 @@ internal sealed class ImageLoadHandler : IDisposable
///
private readonly ActionInt _loadCompleteCallback;
+ ///
+ /// Dedicated lock object (instead of locking on the callback delegate).
+ ///
+ private readonly object _syncLock = new object();
+
///
/// Must be open as long as the image is in use
///
@@ -302,7 +307,7 @@ private void LoadImageFromFile(string source)
try
{
var imageFileStream = File.Open(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- lock (_loadCompleteCallback)
+ lock (_syncLock)
{
_imageFileStream = imageFileStream;
if (!_disposed)
diff --git a/Source/HtmlRenderer/Core/HtmlContainerInt.cs b/Source/HtmlRenderer/Core/HtmlContainerInt.cs
index 1d71de9fb..32d3114c0 100644
--- a/Source/HtmlRenderer/Core/HtmlContainerInt.cs
+++ b/Source/HtmlRenderer/Core/HtmlContainerInt.cs
@@ -460,7 +460,7 @@ public void SetMargins(int value)
///
/// Get the currently selected text segment in the html.
///
- public string SelectedText
+ public string? SelectedText
{
get { return _selectionHandler.GetSelectedText(); }
}
@@ -468,7 +468,7 @@ public string SelectedText
///
/// Copy the currently selected html segment with style.
///
- public string SelectedHtml
+ public string? SelectedHtml
{
get { return _selectionHandler.GetSelectedHtml(); }
}
@@ -504,7 +504,7 @@ internal RColor SelectionBackColor
///
/// the html to init with, init empty if not given
/// optional: the stylesheet to init with, init default if not given
- public void SetHtml(string htmlSource, CssData baseCssData = null)
+ public void SetHtml(string? htmlSource, CssData? baseCssData = null)
{
Clear();
if (!string.IsNullOrEmpty(htmlSource))
@@ -561,7 +561,7 @@ public void ClearSelection()
///
/// Optional: controls the way styles are generated when html is generated (default: )
/// generated html
- public string GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline)
+ public string? GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline)
{
return DomUtils.GenerateHtml(_root, styleGen);
}
@@ -573,7 +573,7 @@ public string GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline)
/// the location to find the attribute at
/// the attribute key to get value by
/// found attribute value or null if not found
- public string GetAttributeAt(RPoint location, string attribute)
+ public string? GetAttributeAt(RPoint location, string attribute)
{
ArgChecker.AssertArgNotNullOrEmpty(attribute, "attribute");
@@ -603,7 +603,7 @@ public List> GetLinks()
///
/// the location to find the link at
/// css link href if exists or null
- public string GetLinkAt(RPoint location)
+ public string? GetLinkAt(RPoint location)
{
var link = DomUtils.GetLinkBox(_root, OffsetByScroll(location));
return link != null ? link.HrefLink : null;
diff --git a/Source/HtmlRenderer/HtmlRenderer.csproj b/Source/HtmlRenderer/HtmlRenderer.csproj
index 273dcccf8..9ea109b51 100644
--- a/Source/HtmlRenderer/HtmlRenderer.csproj
+++ b/Source/HtmlRenderer/HtmlRenderer.csproj
@@ -1,10 +1,10 @@
- netstandard2.0;net8.0
+ net10.0
Library
TheArtOfDev.HtmlRenderer
+ enable
true
- true
@@ -26,6 +26,6 @@ For existing implementations see: HtmlRenderer.WinForms, HtmlRenderer.WPF and Ht
-
+
-
\ No newline at end of file
+