diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.Modern.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.Modern.cs index 90eeab9368b..b2329472da2 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.Modern.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.Modern.cs @@ -14,6 +14,10 @@ public partial class ComboBox private bool _nativeComboHandleInitialized; private bool _normalizingNativeComboBaseline; private int _modernComboLayoutWriteCount; + private HWND _modernSimpleListClipRegionHandle; + private Size _modernSimpleListClipRegionSize; + private int _modernSimpleListClipRegionApplyCount; + private const int ModernSimpleBottomCropLogicalPixels = 2; internal bool UsesModernComboAdapter => EffectiveVisualStylesMode >= VisualStylesMode.Net11 @@ -165,19 +169,6 @@ private ModernComboTargetState ComputeModernComboTargetState() { int topInset = chromeInsets.Top + Padding.Top; int bottomInset = chromeInsets.Bottom + Padding.Bottom; - editBounds.Y += topInset; - - editBounds.Height = Math.Max( - 1, - editBounds.Height - topInset - bottomInset); - - editBounds.Height = Math.Max( - 1, - Math.Min( - editBounds.Height, - ClientRectangle.Bottom - - bottomInset - - editBounds.Top)); // Inset the edit window horizontally so its rectangular corners clear the rounded // field arcs, and reserve the (now wider) drop-down button on the button side. @@ -189,30 +180,88 @@ private ModernComboTargetState ComputeModernComboTargetState() ModernControlVisualStyles.ComboBoxButtonExtraWidth, DeviceDpiInternal); - if (RightToLeft == RightToLeft.Yes) + if (DropDownStyle == ComboBoxStyle.Simple) { - // In RTL the drop-down button sits on the left, so its reservation goes there. - editBounds.X += rightInset + extraButtonWidth; + int dividerThickness = GetModernSimpleDividerThickness(); + int simpleBottomShrink = ScaleHelper.ScaleToDpi( + ModernSimpleBottomCropLogicalPixels, + DeviceDpiInternal); + int simpleListBottom = ClientRectangle.Bottom - simpleBottomShrink - dividerThickness; + int simpleEditTop = topInset + ScaleHelper.ScaleToDpi(1, DeviceDpiInternal); + int selectionFieldHeight = Math.Max( + 1, + (int)PInvokeCore.SendMessage( + this, + PInvoke.CB_GETITEMHEIGHT, + (WPARAM)(-1))); + int minimumReadableHeight = Math.Max( + 1, + FontHeight + ScaleHelper.ScaleToDpi(2, DeviceDpiInternal)); + int preferredSimpleEditHeight = Math.Max(selectionFieldHeight, minimumReadableHeight) + + ScaleHelper.ScaleToDpi(2, DeviceDpiInternal); + int maxSimpleEditHeight = Math.Max( + 1, + simpleListBottom - dividerThickness - simpleEditTop); + + // Keep the editor in the top field lane with enough height for full glyph rendering, + // draw an accent divider below it, and let the permanent list consume the remaining + // space down to the rounded field border. + editBounds.Y = simpleEditTop; + editBounds.Height = Math.Min(preferredSimpleEditHeight, maxSimpleEditHeight); + editBounds.X = leftInset; + editBounds.Width = Math.Max( + 1, + ClientRectangle.Width - leftInset - rightInset); + + if (!simpleListBounds.IsEmpty) + { + simpleListBounds.X = editBounds.Left; + simpleListBounds.Width = editBounds.Width; + simpleListBounds.Y = editBounds.Bottom + dividerThickness; + simpleListBounds.Height = Math.Max( + 1, + simpleListBottom - simpleListBounds.Y); + } } else { - editBounds.X += leftInset; - } - - editBounds.Width = Math.Max( - 1, - editBounds.Width - leftInset - rightInset - extraButtonWidth); + editBounds.Y += topInset; - if (!simpleListBounds.IsEmpty) - { - int simpleListBottom = GetCurrentSimpleListBottom( - simpleListBounds.Bottom); - - simpleListBounds.Y = editBounds.Bottom + bottomInset; + editBounds.Height = Math.Max( + 1, + editBounds.Height - topInset - bottomInset); - simpleListBounds.Height = Math.Max( + editBounds.Height = Math.Max( + 1, + Math.Min( + editBounds.Height, + ClientRectangle.Bottom + - bottomInset + - editBounds.Top)); + + if (RightToLeft == RightToLeft.Yes) + { + // In RTL the drop-down button sits on the left, so its reservation goes there. + editBounds.X += rightInset + extraButtonWidth; + } + else + { + editBounds.X += leftInset; + } + + editBounds.Width = Math.Max( 1, - simpleListBottom - simpleListBounds.Y); + editBounds.Width - leftInset - rightInset - extraButtonWidth); + + if (!simpleListBounds.IsEmpty) + { + int simpleListBottom = GetCurrentSimpleListBottom( + simpleListBounds.Bottom); + simpleListBounds.Y = editBounds.Bottom + bottomInset; + simpleListBounds.Height = Math.Max( + 1, + simpleListBottom - simpleListBounds.Y); + } } } @@ -281,6 +330,60 @@ private void GetAdjustedNativeChildBounds( } } + private void ConfigureModernSimpleListSurface(HWND listHandle) + { + if (DropDownStyle != ComboBoxStyle.Simple + || !UsesModernComboAdapter + || listHandle.IsNull) + { + return; + } + + WINDOW_STYLE style = (WINDOW_STYLE)PInvokeCore.GetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_STYLE); + WINDOW_EX_STYLE exStyle = (WINDOW_EX_STYLE)PInvokeCore.GetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + WINDOW_STYLE updatedStyle = style & ~WINDOW_STYLE.WS_BORDER; + WINDOW_EX_STYLE updatedExStyle = exStyle & ~WINDOW_EX_STYLE.WS_EX_CLIENTEDGE; + + if (updatedStyle == style && updatedExStyle == exStyle) + { + return; + } + + if (updatedStyle != style) + { + PInvokeCore.SetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_STYLE, + (nint)updatedStyle); + } + + if (updatedExStyle != exStyle) + { + PInvokeCore.SetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, + (nint)updatedExStyle); + } + + PInvoke.SetWindowPos( + listHandle, + HWND.Null, + 0, + 0, + 0, + 0, + SET_WINDOW_POS_FLAGS.SWP_NOMOVE + | SET_WINDOW_POS_FLAGS.SWP_NOSIZE + | SET_WINDOW_POS_FLAGS.SWP_NOZORDER + | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE + | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED); + } + private unsafe int GetCurrentSimpleListBottom( int fallback) { @@ -366,6 +469,8 @@ private unsafe void ApplyModernComboLayout() return; } + ConfigureModernSimpleListSurface(comboBoxInfo.hwndList); + if (!target.EditBounds.IsEmpty) { ApplyEditMargins( @@ -464,26 +569,85 @@ private void ApplyEditMargins( private void ApplyChildBounds(HWND childHandle, Rectangle targetBounds) { + bool isModernSimpleList = UsesModernComboAdapter + && DropDownStyle == ComboBoxStyle.Simple + && childHandle == _childListBox?.HWND; + if (childHandle.IsNull - || targetBounds.IsEmpty - || GetChildBounds(childHandle) == targetBounds) + || targetBounds.IsEmpty) { return; } - PInvoke.SetWindowPos( - childHandle, - HWND.Null, - targetBounds.Left, - targetBounds.Top, - targetBounds.Width, - targetBounds.Height, - SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE - | SET_WINDOW_POS_FLAGS.SWP_NOZORDER); + if (GetChildBounds(childHandle) != targetBounds) + { + PInvoke.SetWindowPos( + childHandle, + HWND.Null, + targetBounds.Left, + targetBounds.Top, + targetBounds.Width, + targetBounds.Height, + SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE + | SET_WINDOW_POS_FLAGS.SWP_NOZORDER); - _modernComboLayoutWriteCount++; + _modernComboLayoutWriteCount++; + } + + if (isModernSimpleList) + { + ConfigureModernSimpleListClipRegion( + childHandle, + targetBounds); + } } + private void ConfigureModernSimpleListClipRegion( + HWND listHandle, + Rectangle targetBounds) + { + if (listHandle.IsNull) + { + return; + } + + int bottomShrink = ScaleHelper.ScaleToDpi( + ModernSimpleBottomCropLogicalPixels, + DeviceDpiInternal); + int visibleHeight = Math.Max(1, targetBounds.Height - bottomShrink); + Size targetRegionSize = new(targetBounds.Width, visibleHeight); + + if (listHandle == _modernSimpleListClipRegionHandle + && targetRegionSize == _modernSimpleListClipRegionSize) + { + return; + } + + using RegionScope region = new( + 0, + 0, + targetRegionSize.Width, + targetRegionSize.Height); + + if (PInvoke.SetWindowRgn( + listHandle, + region, + fRedraw: true) != 0) + { + region.RelinquishOwnership(); + _modernSimpleListClipRegionHandle = listHandle; + _modernSimpleListClipRegionSize = targetRegionSize; + _modernSimpleListClipRegionApplyCount++; + } + } + + private int GetModernSimpleDividerThickness() + => Math.Max( + 1, + ScaleHelper.ScaleToDpi( + ModernControlVisualStyles.BorderThickness, + DeviceDpiInternal)); + private Rectangle GetChildBounds(HWND child) { PInvokeCore.GetWindowRect(child, out RECT bounds); @@ -526,6 +690,9 @@ private int GetNativeComboBaselineSelectionFieldItemHeight() private int GetModernComboLayoutWriteCount() => _modernComboLayoutWriteCount; + private int GetModernSimpleListClipRegionApplyCount() + => _modernSimpleListClipRegionApplyCount; + private Padding GetModernFieldPadding() { SystemVisualSettings settings = Application.SystemVisualSettings; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs index 1c0c272383a..393d77e90c0 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs @@ -92,6 +92,7 @@ public override void DrawFlatCombo( graphics, clientBounds); DrawDropDownButton(comboBox, graphics); + DrawSimpleEditListDivider(comboBox, graphics); switch (_flatStyle) { @@ -395,6 +396,45 @@ private static void CutOutRoundedCorners( parentColor); } + private static void DrawSimpleEditListDivider( + ComboBox comboBox, + Graphics graphics) + { + if (comboBox.DropDownStyle != ComboBoxStyle.Simple + || comboBox._childListBox is null) + { + return; + } + + Rectangle listBounds = comboBox.GetChildBounds(comboBox._childListBox.HWND); + if (listBounds.IsEmpty) + { + return; + } + + int dividerThickness = Math.Max( + 1, + ScaleHelper.ScaleToDpi( + ModernControlVisualStyles.BorderThickness, + comboBox.DeviceDpiInternal)); + int lineY = listBounds.Top - dividerThickness; + if (lineY < 0) + { + return; + } + + Padding chromeInsets = comboBox.GetModernChromeInsets(); + int left = chromeInsets.Left + comboBox.Padding.Left; + int right = comboBox.ClientRectangle.Right - chromeInsets.Right - comboBox.Padding.Right - 1; + if (right < left) + { + return; + } + + using var pen = Application.SystemVisualSettings.AccentColor.GetCachedPenScope(dividerThickness); + graphics.DrawLine(pen, left, lineY, right, lineY); + } + private static void DrawRoundedBorder( ComboBox comboBox, Graphics graphics, diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs index 0e2e5c6e0a7..ff2a743304d 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs @@ -71,6 +71,7 @@ public partial class ComboBox : ListControl private short _prefHeightCache = -1; private short _maxDropDownItems = 8; private bool _integralHeight = true; + private bool _integralHeightExplicitlySet; private bool _mousePressed; private bool _mouseEvents; private bool _mouseInEdit; @@ -355,7 +356,10 @@ protected override CreateParams CreateParams cp.Style |= (int)WINDOW_STYLE.WS_VSCROLL | PInvoke.CBS_HASSTRINGS | PInvoke.CBS_AUTOHSCROLL; cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE; - if (!_integralHeight) + if (!_integralHeight + || (UsesModernComboAdapter + && DropDownStyle == ComboBoxStyle.Simple + && !_integralHeightExplicitlySet)) { cp.Style |= PInvoke.CBS_NOINTEGRALHEIGHT; } @@ -491,7 +495,7 @@ public int DropDownHeight // The dropDownHeight is not reflected unless the // ComboBox integralHeight == false.. - IntegralHeight = false; + SetIntegralHeightCore(value: false, userSet: false); } } } @@ -600,13 +604,20 @@ public override Color ForeColor public bool IntegralHeight { get => _integralHeight; - set + set => SetIntegralHeightCore(value, userSet: true); + } + + private void SetIntegralHeightCore(bool value, bool userSet) + { + if (userSet) { - if (_integralHeight != value) - { - _integralHeight = value; - RecreateHandle(); - } + _integralHeightExplicitlySet = true; + } + + if (_integralHeight != value) + { + _integralHeight = value; + RecreateHandle(); } } @@ -2408,6 +2419,12 @@ protected override unsafe void OnHandleCreated(EventArgs e) bool hasComboBoxInfo = PInvoke.GetComboBoxInfo( HWND, ref comboBoxInfo); + + if (hasComboBoxInfo) + { + ConfigureModernSimpleListSurface(comboBoxInfo.hwndList); + } + if (Application.IsDarkModeEnabled) { // Style the ComboBox Open-Button: @@ -2466,6 +2483,9 @@ protected override void OnHandleDestroyed(EventArgs e) _nativeComboHandleInitialized = false; _normalizingNativeComboBaseline = false; _modernComboLayoutWriteCount = 0; + _modernSimpleListClipRegionHandle = HWND.Null; + _modernSimpleListClipRegionSize = Size.Empty; + _modernSimpleListClipRegionApplyCount = 0; _dropDownHandle = HWND.Null; if (Disposing) { @@ -3725,14 +3745,11 @@ protected override unsafe void WndProc(ref Message m) { switch (m.MsgInternal) { - // Modern VisualStyles: expand the client area to the full window so the drop-down - // button, which the themed ComboBox otherwise reserves as non-client (outside - // ClientRectangle), becomes part of the client and is covered by our rounded field. - // Simple combos have no drop-down button (their client already spans the full width - // and hosts a permanent list), so they must never be expanded. + // Modern VisualStyles: expand the client area to the full window so themed non-client + // reservations (for example the ComboBox-hosted scroll strip in Simple mode) are folded + // into client painting and can be normalized by modern layout. case PInvokeCore.WM_NCCALCSIZE: if (UsesModernComboAdapter - && DropDownStyle != ComboBoxStyle.Simple && m.WParamInternal != 0u) { RECT* ncRects = (RECT*)(nint)m.LParamInternal; @@ -3745,11 +3762,11 @@ protected override unsafe void WndProc(ref Message m) base.WndProc(ref m); break; - // Modern VisualStyles: comctl32 still reports the drop-down button region as a - // non-client hit (HTVSCROLL) even though WM_NCCALCSIZE folded it into our client - // area, so a button click would arrive as WM_NCLBUTTONDOWN and never reach the - // WM_LBUTTONDOWN hit-test below. Force HTCLIENT across the expanded client so the - // button click is delivered as a normal client message we can act on. + // Modern VisualStyles: after WM_NCCALCSIZE expands the client area, comctl32 can still + // report the folded strip as non-client (HTVSCROLL). For editable/drop-down-list combos, + // force HTCLIENT across the expanded client so interaction remains consistent with modern + // layout. Keep native hit-testing for Simple to avoid interfering with the hosted list's + // always-visible scrollbar interactions. case PInvokeCore.WM_NCHITTEST: base.WndProc(ref m); if (UsesModernComboAdapter diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs index 8ba593350fd..62cb80a19bc 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs @@ -625,17 +625,20 @@ public void ComboBox_ModernPadding_PositionsEditUsingTopAndBottom( Assert.True( bottomWeightedBounds.Top > topWeightedBounds.Top); - Assert.Equal( - topWeightedBounds.Height, - bottomWeightedBounds.Height); if (dropDownStyle == ComboBoxStyle.Simple) { + Assert.True(bottomWeightedBounds.Height >= control.FontHeight); Rectangle listBounds = control.GetListBounds(); - Assert.Equal( - bottomWeightedBounds.Bottom - + control.ModernChromeInsets.Bottom - + control.Padding.Bottom, - listBounds.Top); + Assert.True( + listBounds.Top + > bottomWeightedBounds.Bottom); + } + else + { + Assert.InRange( + bottomWeightedBounds.Height, + topWeightedBounds.Height - 2, + topWeightedBounds.Height + 2); } } @@ -842,11 +845,14 @@ public void ComboBox_ModernSimple_ResizeRefreshesListBounds() Assert.True( resizedListBounds.Bottom > initialListBounds.Bottom); - Assert.Equal( - control.GetEditBounds().Bottom - + control.ModernChromeInsets.Bottom - + control.Padding.Bottom, - resizedListBounds.Top); + Assert.True( + resizedListBounds.Top + > control.GetEditBounds().Bottom); + Assert.True( + resizedListBounds.Top + <= control.GetEditBounds().Bottom + + control.ModernChromeInsets.Bottom + + control.Padding.Bottom); } [WinFormsFact] @@ -1081,11 +1087,13 @@ public void ComboBox_ModernSimple_FontChangeRecomputesNativeSplit() Rectangle updatedEditBounds = control.GetEditBounds(); Assert.NotEqual(initialEditBounds.Height, updatedEditBounds.Height); - Assert.Equal( - updatedEditBounds.Bottom - + control.ModernChromeInsets.Bottom - + control.Padding.Bottom, - control.GetListBounds().Top); + int listTop = control.GetListBounds().Top; + Assert.True(listTop > updatedEditBounds.Bottom); + Assert.True( + listTop + <= updatedEditBounds.Bottom + + control.ModernChromeInsets.Bottom + + control.Padding.Bottom); int writeCount = control.ModernComboLayoutWriteCount; var state = GetNativeComboState(control); @@ -1213,6 +1221,178 @@ public void ComboBox_ModernChromeInsets_ScaleWithDpi( chromeInsets.Top); } + [WinFormsFact] + public void ComboBox_ModernSimple_EditFillsAvailableFieldWidth() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(180, 120), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.CreateControl(); + + Rectangle editBounds = control.GetEditBounds(); + Padding chromeInsets = control.ModernChromeInsets; + int expectedLeft = chromeInsets.Left; + int expectedRight = control.ClientRectangle.Right - chromeInsets.Right; + + Assert.InRange(editBounds.Left, expectedLeft - 1, expectedLeft + 1); + Assert.InRange(editBounds.Right, expectedRight - 1, expectedRight + 1); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_ListFillsAvailableFieldWidth() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(180, 120), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + Rectangle listBounds = control.GetListBounds(); + Padding chromeInsets = control.ModernChromeInsets; + int expectedLeft = chromeInsets.Left; + int expectedRight = control.ClientRectangle.Right - chromeInsets.Right; + + Assert.True(listBounds.Left <= expectedLeft); + Assert.True(listBounds.Right >= expectedRight - 1); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_EditAndList_DoNotOverlapAndFitFont() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using Font font = new(Control.DefaultFont.FontFamily, 14f); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Font = font, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + Rectangle editBounds = control.GetEditBounds(); + Rectangle listBounds = control.GetListBounds(); + + Assert.True(editBounds.Height >= control.FontHeight); + Assert.True(listBounds.Top > editBounds.Bottom); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_ListIsBorderlessAndFillsRoundedField() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + Rectangle listBounds = control.GetListBounds(); + + Assert.False(control.ListHasBorderStyle()); + Assert.False(control.ListHasClientEdgeExStyle()); + Assert.True(listBounds.Left <= control.ModernChromeInsets.Left); + Assert.True(listBounds.Right >= control.ClientRectangle.Right - control.ModernChromeInsets.Right - 1); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_ApplyModernLayout_ReappliesBorderlessListSurface() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + Assert.False(control.ListHasBorderStyle()); + Assert.False(control.ListHasClientEdgeExStyle()); + + control.ReapplyListBorderAndClientEdgeStyles(); + Assert.True(control.ListHasBorderStyle()); + Assert.True(control.ListHasClientEdgeExStyle()); + + control.ApplyModernComboLayout(); + + Assert.False(control.ListHasBorderStyle()); + Assert.False(control.ListHasClientEdgeExStyle()); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_DrawsAccentDividerBetweenEditAndList() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + using Bitmap bitmap = new(control.Width, control.Height); + + control.DrawToBitmap(bitmap, new Rectangle(Point.Empty, control.Size)); + + Rectangle listBounds = control.GetListBounds(); + int dividerThickness = control.ModernSimpleDividerThickness; + int sampleX = Math.Clamp(control.Width / 2, 0, control.Width - 1); + int startY = Math.Max(0, listBounds.Top - dividerThickness); + int endY = Math.Min(control.Height - 1, listBounds.Top + 1); + bool foundDivider = false; + + for (int y = startY; y <= endY; y++) + { + Color pixel = bitmap.GetPixel(sampleX, y); + if (ColorsAreClose(pixel, Application.SystemVisualSettings.AccentColor, channelTolerance: 3)) + { + foundDivider = true; + break; + } + } + + Assert.True(foundDivider); + } + [WinFormsFact] public void ComboBox_ModernSimple_RepeatedMetricChangeDoesNotAccumulatePadding() { @@ -1266,6 +1446,41 @@ public void ComboBox_ModernSimple_RepeatedMetricChangeDoesNotAccumulatePadding() } } + [WinFormsFact] + public void ComboBox_ModernSimple_ApplyModernLayout_DoesNotReapplyUnchangedClipRegion() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + int initialApplyCount = control.ModernSimpleListClipRegionApplyCount; + + control.ApplyModernComboLayout(); + int stableApplyCount = control.ModernSimpleListClipRegionApplyCount; + + Assert.Equal(initialApplyCount, stableApplyCount); + + control.Height += 20; + + Assert.True(control.ModernSimpleListClipRegionApplyCount > stableApplyCount); + + int resizedApplyCount = control.ModernSimpleListClipRegionApplyCount; + + control.ApplyModernComboLayout(); + + Assert.Equal(resizedApplyCount, control.ModernSimpleListClipRegionApplyCount); + } + [Theory] [InlineData(10, 0, 21999, false)] [InlineData(10, 0, 22000, true)] @@ -1318,6 +1533,49 @@ public void ComboBox_CreateParams_GetDefault_ReturnsExpected() Assert.False(control.IsHandleCreated); } + [WinFormsFact] + public void ComboBox_CreateParams_Net11Simple_ContainsVScroll() + { + using SubComboBox control = new() + { + VisualStylesMode = VisualStylesMode.Net11, + DropDownStyle = ComboBoxStyle.Simple + }; + + CreateParams createParams = control.CreateParams; + + Assert.NotEqual(0, createParams.Style & (int)WINDOW_STYLE.WS_VSCROLL); + } + + [WinFormsFact] + public void ComboBox_CreateParams_Net11Simple_ContainsNoIntegralHeight() + { + using SubComboBox control = new() + { + VisualStylesMode = VisualStylesMode.Net11, + DropDownStyle = ComboBoxStyle.Simple + }; + + CreateParams createParams = control.CreateParams; + + Assert.NotEqual(0, createParams.Style & PInvoke.CBS_NOINTEGRALHEIGHT); + } + + [WinFormsFact] + public void ComboBox_CreateParams_Net11SimpleWithExplicitIntegralHeight_DoesNotForceNoIntegralHeight() + { + using SubComboBox control = new() + { + VisualStylesMode = VisualStylesMode.Net11, + DropDownStyle = ComboBoxStyle.Simple, + IntegralHeight = true + }; + + CreateParams createParams = control.CreateParams; + + Assert.Equal(0, createParams.Style & PInvoke.CBS_NOINTEGRALHEIGHT); + } + [WinFormsTheory] [InvalidEnumData] public void ComboBox_AutoCompleteMode_SetInvalidValue_ThrowsInvalidEnumArgumentException(AutoCompleteMode value) @@ -4046,6 +4304,61 @@ public Rectangle GetButtonBounds() comboBoxInfo.rcButton.Height); } + public bool ListHasBorderStyle() + { + COMBOBOXINFO comboBoxInfo = GetComboBoxInfo(); + WINDOW_STYLE listStyle = (WINDOW_STYLE)PInvokeCore.GetWindowLong( + comboBoxInfo.hwndList, + WINDOW_LONG_PTR_INDEX.GWL_STYLE); + + return (listStyle & WINDOW_STYLE.WS_BORDER) != 0; + } + + public bool ListHasClientEdgeExStyle() + { + COMBOBOXINFO comboBoxInfo = GetComboBoxInfo(); + WINDOW_EX_STYLE listExStyle = (WINDOW_EX_STYLE)PInvokeCore.GetWindowLong( + comboBoxInfo.hwndList, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + return (listExStyle & WINDOW_EX_STYLE.WS_EX_CLIENTEDGE) != 0; + } + + public void ReapplyListBorderAndClientEdgeStyles() + { + COMBOBOXINFO comboBoxInfo = GetComboBoxInfo(); + HWND listHandle = comboBoxInfo.hwndList; + + WINDOW_STYLE style = (WINDOW_STYLE)PInvokeCore.GetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_STYLE); + WINDOW_EX_STYLE exStyle = (WINDOW_EX_STYLE)PInvokeCore.GetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + PInvokeCore.SetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_STYLE, + (nint)(style | WINDOW_STYLE.WS_BORDER)); + PInvokeCore.SetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, + (nint)(exStyle | WINDOW_EX_STYLE.WS_EX_CLIENTEDGE)); + + PInvoke.SetWindowPos( + listHandle, + HWND.Null, + 0, + 0, + 0, + 0, + SET_WINDOW_POS_FLAGS.SWP_NOMOVE + | SET_WINDOW_POS_FLAGS.SWP_NOSIZE + | SET_WINDOW_POS_FLAGS.SWP_NOZORDER + | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE + | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED); + } + private unsafe COMBOBOXINFO GetComboBoxInfo() { COMBOBOXINFO comboBoxInfo = default; @@ -4075,6 +4388,14 @@ public int ModernComboLayoutWriteCount => (int)this.TestAccessor.Dynamic .GetModernComboLayoutWriteCount(); + public int ModernSimpleListClipRegionApplyCount + => (int)this.TestAccessor.Dynamic + .GetModernSimpleListClipRegionApplyCount(); + + public int ModernSimpleDividerThickness + => (int)this.TestAccessor.Dynamic + .GetModernSimpleDividerThickness(); + public int NativeSelectionHeight => (int)this.TestAccessor.Dynamic .GetNativeComboBaselineSelectionFieldItemHeight();