diff --git a/.github/workflows/build-testform.yml b/.github/workflows/build-testform.yml index 6561cf494..4c03836db 100644 --- a/.github/workflows/build-testform.yml +++ b/.github/workflows/build-testform.yml @@ -86,7 +86,7 @@ jobs: ${{ runner.os }}-nuget- - name: Restore TestForm - run: dotnet restore "Source/Krypton Components/TestForm/TestForm.csproj" -p:Configuration=Release -p:TFMs=all + run: dotnet restore "Source/Krypton Components/TestForm/TestForm.csproj" -p:Configuration=Release -p:TFMs=all -p:ExcludeNet11=true - name: Validate TestForm linked resources shell: pwsh @@ -137,7 +137,7 @@ jobs: } - name: Build TestForm - run: dotnet build "Source/Krypton Components/TestForm/TestForm.csproj" -p:Configuration=Release --no-restore -p:TFMs=all -m:1 -p:BuildInParallel=false + run: dotnet build "Source/Krypton Components/TestForm/TestForm.csproj" -p:Configuration=Release --no-restore -p:TFMs=all -p:ExcludeNet11=true -m:1 -p:BuildInParallel=false - name: Save NuGet cache if: success() && github.event_name != 'pull_request' diff --git a/AGENTS.md b/AGENTS.md index 7249da43c..86a5c114e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,7 @@ - Direct VS2026 presets: `.\Scripts\Current\build-stable.cmd`, `.\Scripts\Current\build-canary.cmd`, `.\Scripts\Current\build-nightly.cmd`. - Outputs land under `Bin\\\` by default; with `UseArtifactsOutput=true`, outputs land under `artifacts\bin\\\`. - Target frameworks are selected by MSBuild properties. VS2019/full MSBuild builds only .NET Framework 4.x TFMs; VS2022/full MSBuild excludes `net10.0-windows` and `net11.0-windows`; VS2026/full MSBuild excludes `net11.0-windows` unless explicitly enabled; CI or SDK-based builds can include `net472`, `net48`, `net481`, `net8.0-windows`, `net9.0-windows`, `net10.0-windows`, and `net11.0-windows` when the required SDKs are installed. +- New files must use only the current Standard Toolkit BSD header. Do not add the original ComponentFactory BSD header unless the file is derived from original ComponentFactory source. ## Coding Style & Naming Conventions diff --git a/Documents/Changelog/Changelog.md b/Documents/Changelog/Changelog.md index cbbc98817..6c5c32626 100644 --- a/Documents/Changelog/Changelog.md +++ b/Documents/Changelog/Changelog.md @@ -45,6 +45,7 @@ ## 2026-11-xx - Build 2611 (V110 Nightly) - November 2026 +* Resolved [#3736](https://github.com/Krypton-Suite/Standard-Toolkit/issues/3736), Fixed high-dpi scaling causing magenta image borders * Implemented [#3718](https://github.com/Krypton-Suite/Standard-Toolkit/issues/3718), Use 'switch' expression * Resolved [#3720](https://github.com/Krypton-Suite/Standard-Toolkit/issues/3720), `KryptonTreeView` freezes blank after programmatic `SelectedNode = null` then re-select (drain leaked selection `BeginUpdate`/`EndUpdate` batches from #3282/#3498) * Resolved [#3719](https://github.com/Krypton-Suite/Standard-Toolkit/issues/3719), Information exposure through transmitted data diff --git a/Source/Krypton Components/Krypton.Toolkit/Controls Toolkit/KryptonDataGridView.cs b/Source/Krypton Components/Krypton.Toolkit/Controls Toolkit/KryptonDataGridView.cs index 2fd3c0c28..46557d51c 100644 --- a/Source/Krypton Components/Krypton.Toolkit/Controls Toolkit/KryptonDataGridView.cs +++ b/Source/Krypton Components/Krypton.Toolkit/Controls Toolkit/KryptonDataGridView.cs @@ -183,6 +183,7 @@ private class ToolTipContent : IContentValues private KryptonVScrollBar? _roundingVScrollBar; private KryptonHScrollBar? _roundingHScrollBar; private bool _suppressRoundingScrollSync; + private bool _roundingResizeScrollSyncPending; private int _roundingScrollBarInteractionDepth; private System.Windows.Forms.Timer? _roundingScrollSyncTimer; private string _toolTipText; @@ -1155,10 +1156,7 @@ protected override void OnScroll(ScrollEventArgs e) { base.OnScroll(e); - if (_roundingUsesDetachedScrollbars && !_suppressRoundingScrollSync && _roundingScrollBarInteractionDepth == 0) - { - SyncDetachedRoundingScrollbarsFromGrid(false); - } + SyncDetachedRoundingScrollbarsFromGridIfIdle(false); // #2681 - work-around // Headers not correctly repainted on horizontal mouse scroll @@ -1333,7 +1331,7 @@ or KryptonDataGridViewDomainUpDownCell // the painting to fail. if ((_cellDown.X == -1) || (_cellDown.Y == -1)) { - DoubleBuffered = false; + base.DoubleBuffered = false; } base.OnCellMouseDown(e); @@ -1358,9 +1356,9 @@ protected override void OnCellMouseUp(DataGridViewCellMouseEventArgs e) _cellDown = _nullCell; // Put back double buffered if it was turned off in the OnCellMouseDown - if (!DoubleBuffered) + if (!base.DoubleBuffered) { - DoubleBuffered = true; + base.DoubleBuffered = true; } base.OnCellMouseUp(e); @@ -1876,6 +1874,7 @@ protected override void OnResize(EventArgs e) { base.OnResize(e); UpdateRoundingAppearance(); + QueueDetachedRoundingScrollbarsResizeSync(); } /// @@ -2799,6 +2798,7 @@ private void DisableDetachedRoundingScrollbars() bool wasUsingDetachedScrollbars = _roundingUsesDetachedScrollbars; _roundingScrollBarInteractionDepth = 0; + _roundingResizeScrollSyncPending = false; if (wasUsingDetachedScrollbars) { ShowNativeScrollbarsAfterRounding(); @@ -2981,24 +2981,18 @@ private void SyncDetachedRoundingScrollbarsFromGrid(bool layoutScrollbars) return; } - var hScrollInfo = new WIN32ScrollBars.ScrollInfo - { - cbSize = Marshal.SizeOf(typeof(WIN32ScrollBars.ScrollInfo)), - fMask = (int)PI.SIF_.ALL - }; var vScrollInfo = new WIN32ScrollBars.ScrollInfo { cbSize = Marshal.SizeOf(typeof(WIN32ScrollBars.ScrollInfo)), fMask = (int)PI.SIF_.ALL }; - bool hasHScroll = PI.GetScrollInfo(Handle, PI.SB_.HORZ, ref hScrollInfo); bool hasVScroll = PI.GetScrollInfo(Handle, PI.SB_.VERT, ref vScrollInfo); _suppressRoundingScrollSync = true; try { - UpdateDetachedHorizontalScrollBar(hasHScroll, hScrollInfo); + UpdateDetachedHorizontalScrollBar(); UpdateDetachedVerticalScrollBar(hasVScroll, vScrollInfo); } finally @@ -3009,11 +3003,48 @@ private void SyncDetachedRoundingScrollbarsFromGrid(bool layoutScrollbars) LayoutDetachedRoundingScrollbars(layoutScrollbars); } - private void UpdateDetachedHorizontalScrollBar(bool hasNativeScrollInfo, WIN32ScrollBars.ScrollInfo scrollInfo) + private void SyncDetachedRoundingScrollbarsFromGridIfIdle(bool layoutScrollbars) + { + if (_roundingUsesDetachedScrollbars && !_suppressRoundingScrollSync && _roundingScrollBarInteractionDepth == 0) + { + SyncDetachedRoundingScrollbarsFromGrid(layoutScrollbars); + } + } + + private void QueueDetachedRoundingScrollbarsResizeSync() + { + if (!_roundingUsesDetachedScrollbars + || _roundingResizeScrollSyncPending + || !IsHandleCreated + || IsDisposed + || Disposing) + { + return; + } + + _roundingResizeScrollSyncPending = true; + BeginInvoke((System.Windows.Forms.MethodInvoker)(() => + { + _roundingResizeScrollSyncPending = false; + + if (!_roundingUsesDetachedScrollbars + || !IsHandleCreated + || IsDisposed + || Disposing) + { + return; + } + + HideNativeScrollbarsForRounding(); + SyncDetachedRoundingScrollbarsFromGridIfIdle(true); + })); + } + + private void UpdateDetachedHorizontalScrollBar() { if (_roundingHScrollBar == null || !WantsDetachedHorizontalScrollBar()) { - if (_roundingHScrollBar != null) + if (_roundingHScrollBar != null && _roundingHScrollBar.Visible) { _roundingHScrollBar.Visible = false; } @@ -3021,29 +3052,17 @@ private void UpdateDetachedHorizontalScrollBar(bool hasNativeScrollInfo, WIN32Sc return; } - SetDetachedHorizontalScrollBarBounds(false); - int minimum; int maximum; int page; int position; - if (TryGetNativeDataGridScrollBarMetrics(true, out minimum, out maximum, out page, out position)) - { - ApplyDetachedScrollBarMetrics(_roundingHScrollBar, minimum, maximum, page, position); - return; - } - - if (hasNativeScrollInfo && TryGetNativeScrollMetrics(scrollInfo, out minimum, out maximum, out page, out position)) + if (TryComputeHorizontalScrollMetrics(out minimum, out maximum, out page, out position)) { ApplyDetachedScrollBarMetrics(_roundingHScrollBar, minimum, maximum, page, position); return; } - if (TryComputeHorizontalScrollMetrics(out minimum, out maximum, out page, out position)) - { - ApplyDetachedScrollBarMetrics(_roundingHScrollBar, minimum, maximum, page, position); - } - else + if (_roundingHScrollBar.Visible) { _roundingHScrollBar.Visible = false; } @@ -3053,7 +3072,7 @@ private void UpdateDetachedVerticalScrollBar(bool hasNativeScrollInfo, WIN32Scro { if (_roundingVScrollBar == null || !WantsDetachedVerticalScrollBar()) { - if (_roundingVScrollBar != null) + if (_roundingVScrollBar != null && _roundingVScrollBar.Visible) { _roundingVScrollBar.Visible = false; } @@ -3061,8 +3080,6 @@ private void UpdateDetachedVerticalScrollBar(bool hasNativeScrollInfo, WIN32Scro return; } - SetDetachedVerticalScrollBarBounds(false); - int minimum; int maximum; int page; @@ -3085,7 +3102,10 @@ private void UpdateDetachedVerticalScrollBar(bool hasNativeScrollInfo, WIN32Scro } else { - _roundingVScrollBar.Visible = false; + if (_roundingVScrollBar.Visible) + { + _roundingVScrollBar.Visible = false; + } } } @@ -3339,33 +3359,7 @@ private int GetVerticalScrollPositionInPixels() private int GetHorizontalScrollPositionInPixels() { - var hScrollInfo = new WIN32ScrollBars.ScrollInfo - { - cbSize = Marshal.SizeOf(typeof(WIN32ScrollBars.ScrollInfo)), - fMask = (int)PI.SIF_.POS - }; - - if (IsHandleCreated && PI.GetScrollInfo(Handle, PI.SB_.HORZ, ref hScrollInfo)) - { - return hScrollInfo.nPos; - } - - int position = 0; - int firstColumn = Math.Max(0, FirstDisplayedScrollingColumnIndex); - if (RowHeadersVisible) - { - position += RowHeadersWidth; - } - - for (int i = 0; i < firstColumn && i < Columns.Count; i++) - { - if (Columns[i].Visible) - { - position += Columns[i].Width; - } - } - - return position; + return HorizontalScrollingOffset; } private static int GetNativeScrollableMaximum(int minimum, int maximum, int page) => @@ -4093,6 +4087,14 @@ protected override void WndProc(ref Message m) } #endregion menus + /// + protected override void OnColumnWidthChanged(DataGridViewColumnEventArgs e) + { + base.OnColumnWidthChanged(e); + + SyncDetachedRoundingScrollbarsFromGridIfIdle(false); + } + #region Column ButtonSpec wiring /// /// Wire column-specific ButtonSpec click events when a column is added. diff --git a/Source/Krypton Components/Krypton.Toolkit/Rendering/RenderStandard.cs b/Source/Krypton Components/Krypton.Toolkit/Rendering/RenderStandard.cs index 820037ea0..7d5cb73ba 100644 --- a/Source/Krypton Components/Krypton.Toolkit/Rendering/RenderStandard.cs +++ b/Source/Krypton Components/Krypton.Toolkit/Rendering/RenderStandard.cs @@ -5732,9 +5732,11 @@ private static void AllocateImageSpace([DisallowNull] StandardContentMemento mem float ratio = Math.Min(displayRect.Width / (float)memento.Image.Width, displayRect.Height / (float)memento.Image.Height); + bool avoidPurple = memento.ImageTransparentColor != GlobalStaticVariables.EMPTY_COLOR; + // Resize image to fit display area memento.Image = CommonHelper.ScaleImageForSizedDisplay(memento.Image, memento.Image.Width * ratio, - memento.Image.Height * ratio, false); + memento.Image.Height * ratio, avoidPurple); } if (memento.Image != null) @@ -12395,4 +12397,4 @@ internal void CalculateOverlayImagePosition() } } #endregion -} \ No newline at end of file +} diff --git a/Source/Krypton Components/TestForm/TouchscreenHighDpiDemo.cs b/Source/Krypton Components/TestForm/TouchscreenHighDpiDemo.cs index d2a47cbcd..77899cf4f 100644 --- a/Source/Krypton Components/TestForm/TouchscreenHighDpiDemo.cs +++ b/Source/Krypton Components/TestForm/TouchscreenHighDpiDemo.cs @@ -25,6 +25,7 @@ public partial class TouchscreenHighDpiDemo : KryptonForm { private Timer _dpiMonitorTimer; private bool _updatingFromEvent; + private readonly Dictionary _baselineBounds = new(); public TouchscreenHighDpiDemo() { @@ -48,6 +49,8 @@ private void InitializeForm() // Setup demo controls SetupDemoControls(); + grpSettings.Panel.AutoScroll = true; + // Setup event handlers chkEnableTouchscreen.CheckedChanged += ChkEnableTouchscreen_CheckedChanged; trackScaleFactor.ValueChanged += TrackScaleFactor_ValueChanged; @@ -376,6 +379,8 @@ private void UpdateUIFromSettings() trackFontScaleFactor.Enabled = fontScalingEnabled; lblFontScaleFactor.Enabled = touchscreenEnabled; lblFontScaleValue.Enabled = touchscreenEnabled; + + ApplyScaledDemoLayout(settings); } finally { @@ -383,6 +388,70 @@ private void UpdateUIFromSettings() } } + private void ApplyScaledDemoLayout(TouchscreenSettingValues settings) + { + CaptureBaselineBounds(grpControls.Panel); + CaptureBaselineBounds(grpSettings.Panel); + + float scaleFactor = 1f; + if (settings.TouchscreenModeEnabled) + { + scaleFactor = settings.ControlScaleFactor; + if (settings.FontScalingEnabled) + { + scaleFactor = Math.Max(scaleFactor, settings.FontScaleFactor); + } + } + + scaleFactor = Math.Max(1f, scaleFactor); + + grpControls.Panel.SuspendLayout(); + grpSettings.Panel.SuspendLayout(); + + try + { + foreach (KeyValuePair entry in _baselineBounds) + { + Control control = entry.Key; + if (control.IsDisposed || control.Dock == DockStyle.Fill) + { + continue; + } + + Rectangle bounds = entry.Value; + control.Bounds = new Rectangle( + ScaleLayoutValue(bounds.X, scaleFactor), + ScaleLayoutValue(bounds.Y, scaleFactor), + ScaleLayoutSize(bounds.Width, scaleFactor), + ScaleLayoutSize(bounds.Height, scaleFactor)); + } + } + finally + { + grpSettings.Panel.ResumeLayout(true); + grpControls.Panel.ResumeLayout(true); + } + } + + private void CaptureBaselineBounds(Control parent) + { + foreach (Control child in parent.Controls) + { + if (child.Dock != DockStyle.Fill && !_baselineBounds.ContainsKey(child)) + { + _baselineBounds.Add(child, child.Bounds); + } + + CaptureBaselineBounds(child); + } + } + + private static int ScaleLayoutValue(int value, float scaleFactor) => + Math.Max(0, (int)Math.Round(value * scaleFactor)); + + private static int ScaleLayoutSize(int value, float scaleFactor) => + Math.Max(1, (int)Math.Round(value * scaleFactor)); + private void UpdateStatus() { var settings = KryptonManager.TouchscreenSettingValues;