diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ListView/ListView.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ListView/ListView.cs index f290e8ce8eb..78b96e859fa 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ListView/ListView.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ListView/ListView.cs @@ -113,6 +113,7 @@ public partial class ListView : Control private ImageList? _imageListSmall; private ImageList? _imageListState; private ImageList? _imageListGroup; + private ImageList? _darkModeStateImageList; private MouseButtons _downButton; private int _itemCount; @@ -3066,6 +3067,9 @@ protected override void Dispose(bool disposing) DetachGroupImageListHandlers(); _imageListGroup = null; + _darkModeStateImageList?.Dispose(); + _darkModeStateImageList = null; + // Remove any ColumnHeaders contained in this control if (_columnHeaders is not null) { @@ -4703,9 +4707,84 @@ private void ApplyDarkModeOnDemand() columnHeaderHandle, $"{DarkModeIdentifier}_{ItemsViewThemeIdentifier}", null); + + if (CheckBoxes) + { + UpdateDarkModeCheckBoxImages(); + } + } + } + + private void UpdateDarkModeCheckBoxImages() + { + if (_imageListState is not null || !CheckBoxes || !IsHandleCreated) + { + return; + } + + _darkModeStateImageList?.Dispose(); + + int size = ScaleHelper.ScaleToInitialSystemDpi(16); + + ImageList imageList = new() + { + ImageSize = new Size(size, size), + ColorDepth = ColorDepth.Depth32Bit, + TransparentColor = Color.Transparent + }; + + imageList.Images.Add(CreateDarkModeCheckBoxBitmap(isChecked: false, size)); + imageList.Images.Add(CreateDarkModeCheckBoxBitmap(isChecked: true, size)); + + // Ensure the checkbox extended style is active before assigning our custom state imagelist, + // otherwise the style refresh may overwrite the list with the default light-themed images. + ForceCheckBoxUpdate(); + + PInvokeCore.SendMessage( + this, + PInvoke.LVM_SETIMAGELIST, + (WPARAM)PInvoke.LVSIL_STATE, + (LPARAM)imageList.Handle); + + _darkModeStateImageList = imageList; + + if (VirtualMode) + { + return; + } + + if (Items.Count > 0) + { + const LIST_VIEW_ITEM_STATE_FLAGS stateMask = LIST_VIEW_ITEM_STATE_FLAGS.LVIS_STATEIMAGEMASK; + + for (int i = 0; i < Items.Count; i++) + { + bool isChecked = Items[i].Checked; + int stateIndex = isChecked ? 2 : 1; + + SetItemState(i, (LIST_VIEW_ITEM_STATE_FLAGS)(stateIndex << 12), stateMask); + } + + PInvokeCore.SendMessage( + this, + PInvoke.LVM_REDRAWITEMS, + (WPARAM)0, + (LPARAM)(Items.Count - 1)); } } + private Bitmap CreateDarkModeCheckBoxBitmap(bool isChecked, int size) + { + Bitmap bitmap = new(size, size); + + using Graphics graphics = Graphics.FromImage(bitmap); + graphics.Clear(Color.Transparent); + CheckBoxState state = isChecked ? CheckBoxState.CheckedNormal : CheckBoxState.UncheckedNormal; + CheckBoxRenderer.DrawCheckBoxWithVisualStyles(graphics, new Point(0, 0), state, HWNDInternal); + + return bitmap; + } + protected override void OnHandleDestroyed(EventArgs e) { // don't save the list view items state when in virtual mode : it is the responsibility of the diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs index 4d6a3c53602..6958d9ec35f 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs @@ -284,6 +284,7 @@ public bool CheckBoxes if (CheckBoxes) { UpdateStyles(); + UpdateCheckBoxImages(); } else { @@ -1869,6 +1870,7 @@ protected override void OnHandleCreated(EventArgs e) int style = (int)PInvokeCore.GetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_STYLE); style |= (int)PInvoke.TVS_CHECKBOXES; PInvokeCore.SetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style); + UpdateCheckBoxImages(); } if (ShowNodeToolTips && !DesignMode) @@ -1991,6 +1993,43 @@ private void UpdateNativeStateImageList() _internalStateImageList = newImageList; } + private void UpdateCheckBoxImages() + { + if (DesignMode || !IsHandleCreated || !CheckBoxes || _stateImageList is not null) + { + return; + } + + int size = StateImageSize?.Width ?? ScaleHelper.ScaleToInitialSystemDpi(16); + ImageList imageList = new() + { + ImageSize = new Size(size, size), + ColorDepth = ColorDepth.Depth32Bit + }; + + using Bitmap uncheckedBitmap = new(size, size); + using (Graphics g = Graphics.FromImage(uncheckedBitmap)) + { + CheckBoxRenderer.DrawCheckBoxWithVisualStyles(g, new Point(0, 0), CheckBoxState.UncheckedNormal, HWNDInternal); + } + + using Bitmap checkedBitmap = new(size, size); + using (Graphics g = Graphics.FromImage(checkedBitmap)) + { + CheckBoxRenderer.DrawCheckBoxWithVisualStyles(g, new Point(0, 0), CheckBoxState.CheckedNormal, HWNDInternal); + } + + // TreeView uses 1-based indexing: index 0=placeholder, 1=unchecked, 2=checked + imageList.Images.Add(new Bitmap(size, size)); // Placeholder + imageList.Images.Add(uncheckedBitmap); + imageList.Images.Add(checkedBitmap); + + SetStateImageList(imageList.Handle); + + _internalStateImageList?.Dispose(); + _internalStateImageList = imageList; + } + private void SetStateImageList(IntPtr handle) { // In certain cases (TREEVIEWSTATE_checkBoxes) e.g., the Native TreeView leaks the imageList @@ -3385,6 +3424,7 @@ protected override unsafe void WndProc(ref Message m) case PInvokeCore.WM_SYSCOLORCHANGE: PInvokeCore.SendMessage(this, PInvoke.TVM_SETINDENT, (WPARAM)Indent); base.WndProc(ref m); + UpdateCheckBoxImages(); break; case PInvokeCore.WM_SETFOCUS: // If we get focus through the LButtonDown .. we might have done the validation...