Skip to content

Commit 7be132c

Browse files
committed
* Ribbon MDI Fix
# Resolve #2921 – Double ribbon with MDI; caption/QAT hit-test misalignment ## Summary Fixes [Issue #2921](#2921): when a `KryptonRibbon` is used on an MDI container (`KryptonForm` with `IsMdiContainer = true`), the following occurred: 1. **Double ribbon tabs** – Two identical rows of ribbon tabs (File, Tab) appeared, briefly when opening a maximized MDI child and sometimes persisting after closing all MDI children. 2. **Misaligned hit areas** – Close, minimize, and maximize buttons (and QAT) had their clickable areas shifted downward relative to the drawn buttons, so clicks did not register where the user clicked. 3. **Caption drag** – The area used for dragging the window (caption) did not align with the ribbon’s spare caption area. This PR addresses all three by fixing coordinate usage for caption hit-testing and by re-syncing caption/ribbon state when the last MDI child is closed. It also adds a TestForm demo to reproduce and verify the scenario. --- ## Root cause - **Hit-test mismatch:** `CustomCaptionArea` (set by the ribbon in **form client** coordinates) was hit-tested using **window** coordinates. The form uses `ScreenToWindow` for NC hit-test, so the point was in window space while the rectangle was in client space, causing the perceived downward shift for caption drag, close/min/max, and QAT. - **Double tabs:** When the ribbon integrated into the form caption, context titles were in both the caption view and the ribbon view hierarchy; removing them from the caption area before injecting into the form (existing #2921 fix) avoids double layout/draw. When the last MDI child closed, caption/ribbon state was not refreshed, so double tabs could remain until a full re-layout. --- ## Changes ### Krypton.Toolkit - **`KryptonForm.cs`** - **`WindowChromeHitTest`:** Before testing `CustomCaptionArea.Contains(pt)`, convert the hit-test point from window coordinates to client coordinates using `RealWindowBorders`, so caption drag (and ribbon spare area) align with the drawn caption and buttons. - **`WindowChromeHitTest`:** Use a single `Padding borders = RealWindowBorders` declaration for the method and remove the duplicate declaration that caused CS0136. ### Krypton.Ribbon - **`ViewLayoutRibbonTabsArea.cs`** - **`OnRibbonMdiChildActivate`:** When the active MDI child becomes `null` (last child closed) and the top-level form is a `KryptonForm`, call `_captionArea.PerformFormChromeCheck()`, `kryptonForm.RecreateMinMaxCloseButtons()`, `kryptonForm.PerformNeedPaint(true)`, and `kryptonForm.InvalidateNonClient()` so caption integration and layout are re-applied and double tabs do not persist. *(Existing fix in `ViewDrawRibbonCaptionArea.cs` for #2921 – removing context titles from the caption area before injecting into the form – remains; it prevents the view from being in two hierarchies and thus avoids double layout/draw.)* ### TestForm (demo) - **New: Ribbon MDI Demo (Issue #2921)** - **`RibbonMdiDemo.cs`** – `KryptonForm` MDI container that creates a `KryptonRibbon` in `Load`, sets QAT above and `RibbonFileAppButton.AppButtonVisible`, adds File and Home tabs, and adds the ribbon as the first control (`Controls.Add` + `SetChildIndex(..., 0)`) for caption integration. - **`RibbonMdiDemo.Designer.cs`** – Tool strip (Add Resizable Child, Add No-Resize Child, Open Maximized, Close All, Tile Horizontally/Vertically, Cascade) and status strip with verification text for #2921. - **`RibbonMdiChildForm.cs`** – MDI child that can be resizable (`FormBorderStyle.Sizable`) or no-resize (`FixedSingle`), with short instructions for testing close/min/max and QAT alignment. - **`StartScreen.cs`** – New button: “Ribbon MDI Demo (Issue #2921)” with description pointing to the issue and what to verify. ### Krypton.Utilities (unrelated build fix) - **`Krypton.Utilities.csproj`** - WebView2 `PackageReference` version range updated from `(1.0.0,2.0.0)` to `[1.0.0,2.0.0)` for an inclusive lower bound so restore resolves correctly (addresses NU1604/CS0234 when WebView2 was missing). - For .NET 8+, `WEBVIEW2_AVAILABLE` is defined only when the file-based WebView2 SDK exists (`Exists('$(WebView2CoreDll)')`); otherwise the stub compiles and CS0234 is avoided. - **`KryptonWebView2.cs`** - Added an `#else` stub implementation of `KryptonWebView2` (inheriting `Control`) when `WEBVIEW2_AVAILABLE` is not defined, so the project builds when WebView2 is not present. --- ## How to verify 1. Run TestForm (e.g. `dotnet run --project "Source/Krypton Components/TestForm/TestForm.csproj" -c Debug -f net8.0-windows`). 2. Open **“Ribbon MDI Demo (Issue #2921)”** from the start screen. 3. Use **Open Maximized** (or **Add Resizable Child** then maximize) – confirm only one row of ribbon tabs (no double File/Tab). 4. Use **Close All** – confirm double tabs do not remain; single ribbon row only. 5. Use **Add No-Resize Child** (or resizable) – confirm close, minimize, and maximize buttons and QAT respond when clicking on the **drawn** buttons (no downward shift). 6. Drag the window by the caption area next to the ribbon – confirm it moves as expected. --- ## Checklist - [x] Code builds (TestForm and affected projects). - [x] Hit-test uses client coordinates for `CustomCaptionArea` so caption and buttons align. - [x] Last MDI child close triggers caption re-check and invalidation to prevent persistent double tabs. - [x] Ribbon MDI demo added and registered in StartScreen. - [x] No new linter errors introduced in changed files.
1 parent e68a4fb commit 7be132c

File tree

8 files changed

+403
-13
lines changed

8 files changed

+403
-13
lines changed

Source/Krypton Components/Krypton.Ribbon/View Layout/ViewLayoutRibbonTabsArea.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region BSD License
1+
#region BSD License
22
/*
33
* Original BSD 3-Clause License (https://github.com/ComponentFactory/Krypton/blob/master/LICENSE)
44
* © Component Factory Pty Ltd, 2006 - 2016, All rights reserved.
@@ -629,6 +629,16 @@ private void OnRibbonMdiChildActivate(object? sender, EventArgs e)
629629
ButtonSpecManager?.RecreateButtons();
630630
PerformNeedPaint(true);
631631

632+
// Issue #2921: When last MDI child closes, re-check caption integration and force form
633+
// to re-layout so double ribbon / stale caption does not remain.
634+
if (_activeMdiChild == null && topForm is KryptonForm kryptonForm)
635+
{
636+
_captionArea.PerformFormChromeCheck();
637+
kryptonForm.RecreateMinMaxCloseButtons();
638+
kryptonForm.PerformNeedPaint(true);
639+
kryptonForm.InvalidateNonClient();
640+
}
641+
632642
// We never want the mdi child window to have a system menu, we provide the
633643
// pendant buttons as part of the ribbon and so replace the need for it.
634644
PI.SetMenu(new HandleRef(_ribbon, topForm!.Handle), NullHandleRef);

Source/Krypton Components/Krypton.Toolkit/Controls Toolkit/KryptonForm.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,9 +2031,17 @@ protected override IntPtr WindowChromeHitTest(Point pt)
20312031
return new IntPtr(PI.HT.REDUCE);
20322032
}
20332033

2034-
if (CustomCaptionArea.Contains(pt))
2034+
Padding borders = RealWindowBorders;
2035+
2036+
// Issue #2921: CustomCaptionArea is in form client coordinates (set by ribbon);
2037+
// hit-test pt is in window coordinates — convert for correct caption/drag detection.
2038+
if (!CustomCaptionArea.IsEmpty)
20352039
{
2036-
return new IntPtr(PI.HT.CAPTION);
2040+
var clientPt = new Point(pt.X - borders.Left, pt.Y - borders.Top);
2041+
if (CustomCaptionArea.Contains(clientPt))
2042+
{
2043+
return new IntPtr(PI.HT.CAPTION);
2044+
}
20372045
}
20382046

20392047
// Do not allow the caption to be moved or the border resized
@@ -2067,7 +2075,6 @@ protected override IntPtr WindowChromeHitTest(Point pt)
20672075
}
20682076

20692077
bool isResizable = FormBorderStyle is FormBorderStyle.Sizable or FormBorderStyle.SizableToolWindow;
2070-
Padding borders = RealWindowBorders;
20712078

20722079
// Material: use a wider invisible hit band for easier resize while keeping flat, borderless visuals.
20732080
// RealWindowBorders can be 0 when the palette (e.g., Material) suppresses border width for drawing.

Source/Krypton Components/Krypton.Utilities/Components/KryptonWebView2/Controls Toolkit/KryptonWebView2.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,4 +681,24 @@ protected virtual void OnGlobalPaletteChanged(object? sender, EventArgs e)
681681
#endregion
682682
}
683683

684+
#else
685+
686+
using System.ComponentModel;
687+
using System.Drawing;
688+
using System.Windows.Forms;
689+
690+
/// <summary>
691+
/// Stub when WebView2 SDK/package is not available. Run dotnet restore or add WebView2 SDK for full implementation.
692+
/// </summary>
693+
[ToolboxItem(true)]
694+
[ToolboxBitmap(typeof(KryptonWebView2), "KryptonWebView2.ToolboxBitmaps.WebView2.bmp")]
695+
[Description(@"WebView2 with Krypton theming. WebView2 SDK or package required; run dotnet restore or add WebView2 SDK.")]
696+
public class KryptonWebView2 : Control
697+
{
698+
public KryptonWebView2()
699+
{
700+
BackColor = SystemColors.Control;
701+
}
702+
}
703+
684704
#endif

Source/Krypton Components/Krypton.Utilities/Krypton.Utilities.csproj

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@
6262
</PropertyGroup>
6363

6464
<!-- WebView2 NuGet Package Reference for .NET Framework targets (4.7.2+) -->
65-
<!-- Uses minimum version with floating patch to automatically resolve to latest 1.0.x version -->
65+
<!-- Inclusive lower bound [1.0.0,2.0.0) so restore resolves and assembly reference is available -->
6666
<ItemGroup Condition="'$(TargetFramework)' == 'net472' Or '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'net481'">
67-
<PackageReference Include="Microsoft.Web.WebView2" Version="(1.0.0,2.0.0)" />
67+
<PackageReference Include="Microsoft.Web.WebView2" Version="[1.0.0,2.0.0)" />
6868
</ItemGroup>
6969

7070
<!-- WebView2 SDK References for .NET 8+ (using file-based references if available) -->
@@ -81,19 +81,18 @@
8181
</ItemGroup>
8282

8383
<!-- NuGet package for .NET 8+ (always added; file-based refs take precedence when available) -->
84-
<!-- Uses minimum version with floating patch to automatically resolve to latest 1.0.x version -->
84+
<!-- Inclusive lower bound [1.0.0,2.0.0) so restore resolves and assembly reference is available -->
8585
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0-windows' Or '$(TargetFramework)' == 'net9.0-windows' Or '$(TargetFramework)' == 'net10.0-windows'">
86-
<PackageReference Include="Microsoft.Web.WebView2" Version="(1.0.0,2.0.0)" />
86+
<PackageReference Include="Microsoft.Web.WebView2" Version="[1.0.0,2.0.0)" />
8787
</ItemGroup>
8888

89-
<!-- Define WEBVIEW2_AVAILABLE when WebView2 is available for this TFM -->
90-
<!-- For .NET Framework: always available via NuGet package -->
89+
<!-- Define WEBVIEW2_AVAILABLE only when a WebView2 reference is present to avoid CS0234 (missing Microsoft.Web). -->
90+
<!-- .NET Framework: PackageReference added for these TFMs; run dotnet restore before build. -->
9191
<PropertyGroup Condition="'$(TargetFramework)' == 'net472' Or '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'net481'">
9292
<DefineConstants>$(DefineConstants);WEBVIEW2_AVAILABLE</DefineConstants>
9393
</PropertyGroup>
94-
<!-- For .NET 8+: always available via either file-based refs (when SDK exists) or PackageReference fallback -->
95-
<!-- Note: Ensure restore runs before build to resolve PackageReference when SDK files don't exist -->
96-
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0-windows' Or '$(TargetFramework)' == 'net9.0-windows' Or '$(TargetFramework)' == 'net10.0-windows'">
94+
<!-- .NET 8+: only when file-based SDK exists (Reference guaranteed). Without SDK, stub compiles; run restore and ensure WebView2 package for full impl. -->
95+
<PropertyGroup Condition="('$(TargetFramework)' == 'net8.0-windows' Or '$(TargetFramework)' == 'net9.0-windows' Or '$(TargetFramework)' == 'net10.0-windows') And Exists('$(WebView2CoreDll)')">
9796
<DefineConstants>$(DefineConstants);WEBVIEW2_AVAILABLE</DefineConstants>
9897
</PropertyGroup>
9998

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#region BSD License
2+
/*
3+
* New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
4+
* Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), et al. 2017 - 2026. All rights reserved.
5+
*/
6+
#endregion
7+
8+
using System.Drawing;
9+
using System.Windows.Forms;
10+
using Krypton.Toolkit;
11+
12+
namespace TestForm;
13+
14+
/// <summary>
15+
/// MDI child form for Ribbon MDI demo (Issue #2921).
16+
/// Can be created as resizable (Sizable) or no-resize (FixedSingle) to test both scenarios.
17+
/// </summary>
18+
public class RibbonMdiChildForm : KryptonForm
19+
{
20+
private readonly bool _resizable;
21+
private KryptonLabel? _label;
22+
23+
public RibbonMdiChildForm(bool resizable)
24+
{
25+
_resizable = resizable;
26+
InitializeComponent();
27+
}
28+
29+
private void InitializeComponent()
30+
{
31+
SuspendLayout();
32+
AutoScaleDimensions = new SizeF(6F, 13F);
33+
AutoScaleMode = AutoScaleMode.Font;
34+
ClientSize = new Size(400, 280);
35+
FormBorderStyle = _resizable ? FormBorderStyle.Sizable : FormBorderStyle.FixedSingle;
36+
MaximizeBox = true;
37+
MinimizeBox = true;
38+
Name = "RibbonMdiChildForm";
39+
StartPosition = FormStartPosition.CenterParent;
40+
Text = _resizable ? "Resize Test Window" : "No Resize";
41+
//
42+
// Label with instructions
43+
//
44+
_label = new KryptonLabel
45+
{
46+
Dock = DockStyle.Fill,
47+
Text = _resizable
48+
? "Resizable MDI child.\r\n\r\n• Use title bar close/min/max — hit areas should match the buttons.\r\n• Open another child as Maximized to check for double ribbon tabs.\r\n• Close all children — double tabs should not remain."
49+
: "No-Resize MDI child (FixedSingle).\r\n\r\n• Use title bar close/min/max — hit areas should match the buttons.\r\n• QAT and caption drag should align with visuals (Issue #2921).",
50+
LabelStyle = LabelStyle.TitlePanel,
51+
StateCommon =
52+
{
53+
LongText =
54+
{
55+
TextH = PaletteRelativeAlign.Center,
56+
TextV = PaletteRelativeAlign.Center,
57+
Font = new Font("Segoe UI", 10F)
58+
}
59+
}
60+
};
61+
Controls.Add(_label);
62+
ResumeLayout(false);
63+
}
64+
}

Source/Krypton Components/TestForm/RibbonMdiDemo.Designer.cs

Lines changed: 179 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)