From 8b3b78113f2426f0b127340e1187ef55b2275ab6 Mon Sep 17 00:00:00 2001 From: kushan <9400651+kushanp@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:12:47 +0530 Subject: [PATCH 1/3] feat: Modernize HtmlRenderer.Core and HtmlRenderer.WinForms for .NET 10 --- .../Adapters/WinFormsAdapter.cs | 2 +- Source/HtmlRenderer.WinForms/HtmlContainer.cs | 12 ++--- Source/HtmlRenderer.WinForms/HtmlLabel.cs | 24 ++++----- Source/HtmlRenderer.WinForms/HtmlPanel.cs | 18 +++---- .../HtmlRenderer.WinForms.csproj | 4 +- Source/HtmlRenderer.WinForms/HtmlToolTip.cs | 54 ++++++++++--------- .../MetafileExtensions.cs | 19 +++---- .../HtmlRenderer.WinForms/Utilities/Utils.cs | 2 + .../Utilities/Win32Utils.cs | 8 +-- .../HtmlRenderer/Adapters/Entities/RColor.cs | 2 +- .../HtmlRenderer/Adapters/Entities/RPoint.cs | 2 +- .../HtmlRenderer/Adapters/Entities/RRect.cs | 2 +- .../HtmlRenderer/Adapters/Entities/RSize.cs | 2 +- Source/HtmlRenderer/Adapters/RAdapter.cs | 2 +- Source/HtmlRenderer/Core/Entities/CssBlock.cs | 2 +- .../Core/Handlers/ImageDownloader.cs | 30 +++++++---- .../Core/Handlers/ImageLoadHandler.cs | 7 ++- Source/HtmlRenderer/Core/HtmlContainerInt.cs | 12 ++--- Source/HtmlRenderer/HtmlRenderer.csproj | 4 +- 19 files changed, 111 insertions(+), 97 deletions(-) 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..a4fafd054 100644 --- a/Source/HtmlRenderer.WinForms/HtmlPanel.cs +++ b/Source/HtmlRenderer.WinForms/HtmlPanel.cs @@ -77,17 +77,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 @@ -304,7 +304,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; } @@ -471,13 +471,9 @@ protected void PerformHtmlLayout() { _htmlContainer.MaxSize = new SizeF(ClientSize.Width - Padding.Horizontal, 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)); @@ -747,7 +743,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..04ffe777f 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 From 981bb4c383d17bd6a73dc96cfb24c952c5a3c4a7 Mon Sep 17 00:00:00 2001 From: kushan <9400651+kushanp@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:24:56 +0530 Subject: [PATCH 2/3] chore: bump System.Resources.Extensions to 10.0.10 for .NET 10 Keep the Core package reference aligned with the latest .NET 10 servicing release. --- Source/HtmlRenderer/HtmlRenderer.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/HtmlRenderer/HtmlRenderer.csproj b/Source/HtmlRenderer/HtmlRenderer.csproj index 04ffe777f..9ea109b51 100644 --- a/Source/HtmlRenderer/HtmlRenderer.csproj +++ b/Source/HtmlRenderer/HtmlRenderer.csproj @@ -26,6 +26,6 @@ For existing implementations see: HtmlRenderer.WinForms, HtmlRenderer.WPF and Ht - + - \ No newline at end of file + From 972b9ddd3f607bf8726d483b3437fd38cfc55445 Mon Sep 17 00:00:00 2001 From: kushan <9400651+kushanp@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:28:31 +0530 Subject: [PATCH 3/3] Add HtmlPanel zoom (Ctrl+wheel) and middle-mouse pan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement browser-style zoom with anchor-to-cursor on Ctrl+MouseWheel, optional Ctrl+/−/0 shortcuts, and middle-button drag panning. Scale paint via Graphics transform and force GDI+ text when zoomed so glyphs scale (GDI TextOut ignores ScaleTransform). --- .../Adapters/GraphicsAdapter.cs | 23 +- Source/HtmlRenderer.WinForms/HtmlPanel.cs | 338 ++++++++++++++++-- 2 files changed, 336 insertions(+), 25 deletions(-) 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/HtmlPanel.cs b/Source/HtmlRenderer.WinForms/HtmlPanel.cs index a4fafd054..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 { @@ -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. /// @@ -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,20 +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); using (var g = CreateGraphics()) { _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)); } } @@ -491,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)) @@ -513,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)); } /// @@ -528,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(); } /// @@ -602,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. /// @@ -616,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. /// @@ -685,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(); } /// @@ -699,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 @@ -710,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 . ///