Skip to content

Commit 76e7021

Browse files
Fix rectangle of the ListViewGroup (#4800)
Fixes #4778 ## Proposed changes - The issue with the incorrect rectangle is reproduced because the "FragmentRoot" property was not overridden. Added an override for the "FragmentRoot" property. - Fixed issue with getting a rectangle for a ListViewGroup. Now, instead of the ListViewGroup index, we use the group ID. Fixed a issue with getting an incorrect ListViewGroup index. - Added unit tests. Fixed typos in unit-tests naming <!-- We are in TELL-MODE the following section must be completed --> ## Customer Impact ### Case 1 **Before fix:** ![Issue-4778-case1-before](https://user-images.githubusercontent.com/23376742/114997193-41c42500-9ea8-11eb-9d8d-dd2fed7be588.png) **After fix:** ![Issue-4778-case1-after](https://user-images.githubusercontent.com/23376742/114997486-8e0f6500-9ea8-11eb-8e14-0aae753335ac.png) ### Case 2 **Before fix:** ![Issue-4778-case2-before](https://user-images.githubusercontent.com/23376742/114997915-0bd37080-9ea9-11eb-8203-d76619915de9.png) **After fix:** ![Issue-4778-case2-after](https://user-images.githubusercontent.com/23376742/114997927-1130bb00-9ea9-11eb-80fe-bee59783f3cf.png) ## Regression? - Yes (from #3224) ## Risk - Minimal ## Test methodology <!-- How did you ensure quality? --> - CTI team - unit tests ## Accessibility testing <!-- Remove this section if PR does not change UI --> - Inspector ## Test environment(s) <!-- Remove any that don't apply --> - Microsoft Windows [Version 10.0.19041.388] - .NET Core SDK: 6.0.100-preview.2.21155.3
1 parent 7ecc0cf commit 76e7021

8 files changed

Lines changed: 276 additions & 85 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
internal static partial class Interop
6+
{
7+
internal static partial class ComCtl32
8+
{
9+
public enum LVGGR
10+
{
11+
GROUP = 0,
12+
HEADER = 1,
13+
LABEL = 2,
14+
SUBSETLINK = 3
15+
}
16+
}
17+
}

src/System.Windows.Forms/src/System/Windows/Forms/ListView.ListViewAccessibleObject.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ internal override bool CanSelectMultiple
2626
internal override int ColumnCount
2727
=> _owningListView.Columns.Count;
2828

29-
private bool OwnerHasDefaultGroup
29+
internal bool OwnerHasDefaultGroup
3030
{
3131
get
3232
{
33-
if (!_owningListView.IsHandleCreated || !_owningListView.ShowGroups || _owningListView.VirtualMode)
33+
if (!_owningListView.ShowGroups || _owningListView.VirtualMode)
3434
{
3535
return false;
3636
}
@@ -77,7 +77,7 @@ internal override int[]? RuntimeId
7777
}
7878

7979
// ListViewGroup are not displayed when the ListView is in "List" view
80-
private bool ShowGroupAccessibleObject => _owningListView.View != View.List && _owningListView.GroupsEnabled;
80+
internal bool ShowGroupAccessibleObject => _owningListView.View != View.List && _owningListView.GroupsEnabled;
8181

8282
internal override UiaCore.IRawElementProviderFragment? ElementProviderFromPoint(double x, double y)
8383
{

src/System.Windows.Forms/src/System/Windows/Forms/ListView.cs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3501,22 +3501,6 @@ internal int GetColumnIndex(ColumnHeader ch)
35013501
return -1;
35023502
}
35033503

3504-
internal int GetGroupIndex(ListViewGroup owningGroup)
3505-
{
3506-
if (Groups.Count == 0)
3507-
{
3508-
return -1;
3509-
}
3510-
3511-
// Default group it's last group in accessibility and not specified in Groups collection, therefore default group index = Groups.Count
3512-
if (DefaultGroup == owningGroup)
3513-
{
3514-
return Groups.Count;
3515-
}
3516-
3517-
return Groups.IndexOf(owningGroup);
3518-
}
3519-
35203504
/// <summary>
35213505
/// Returns the current ListViewItem corresponding to the specific
35223506
/// x,y co-ordinate.

src/System.Windows.Forms/src/System/Windows/Forms/ListViewGroup.ListViewGroupAccessibleObject.cs

Lines changed: 81 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Collections.Generic;
66
using System.Drawing;
7+
using System.Runtime.InteropServices;
78
using static System.Windows.Forms.ListView;
89
using static Interop;
910
using static Interop.ComCtl32;
@@ -42,28 +43,55 @@ public override Rectangle Bounds
4243
{
4344
get
4445
{
45-
if (!_owningListView.IsHandleCreated || _owningListView.VirtualMode)
46+
if (!_owningListView.IsHandleCreated || !_owningListViewAccessibilityObject.ShowGroupAccessibleObject)
4647
{
4748
return Rectangle.Empty;
4849
}
4950

50-
RECT groupRect = new RECT();
51-
User32.SendMessageW(_owningListView, (User32.WM)ComCtl32.LVM.GETGROUPRECT, (IntPtr)CurrentIndex, ref groupRect);
51+
if (GetVisibleItems().Count == 0)
52+
{
53+
return Rectangle.Empty;
54+
}
55+
56+
int nativeGroupId = GetNativeGroupId();
57+
if (nativeGroupId == -1)
58+
{
59+
return Rectangle.Empty;
60+
}
5261

53-
return new Rectangle(
54-
_owningListViewAccessibilityObject.Bounds.X + groupRect.left,
55-
_owningListViewAccessibilityObject.Bounds.Y + groupRect.top,
56-
groupRect.right - groupRect.left,
57-
groupRect.bottom - groupRect.top);
62+
LVGGR rectType = _owningGroup.CollapsedState == ListViewGroupCollapsedState.Collapsed
63+
? LVGGR.HEADER
64+
: LVGGR.GROUP;
65+
66+
// Get the native rectangle
67+
RECT groupRect = new();
68+
69+
// Using the "top" property, we set which rectangle type of the group we want to get
70+
// This is described in more detail in https://docs.microsoft.com/windows/win32/controls/lvm-getgrouprect
71+
groupRect.top = (int)rectType;
72+
User32.SendMessageW(_owningListView, (User32.WM)LVM.GETGROUPRECT, (IntPtr)nativeGroupId, ref groupRect);
73+
74+
// Using the following code, we limit the size of the ListViewGroup rectangle
75+
// so that it does not go beyond the rectangle of the ListView
76+
Rectangle listViewBounds = _owningListView.AccessibilityObject.Bounds;
77+
groupRect = _owningListView.RectangleToScreen(groupRect);
78+
groupRect.top = Math.Max(listViewBounds.Top, groupRect.top);
79+
groupRect.bottom = Math.Min(listViewBounds.Bottom, groupRect.bottom);
80+
groupRect.left = Math.Max(listViewBounds.Left, groupRect.left);
81+
groupRect.right = Math.Min(listViewBounds.Right, groupRect.right);
82+
83+
return groupRect;
5884
}
5985
}
6086

61-
private int CurrentIndex
87+
internal int CurrentIndex
88+
// The default group has 0 index, as it is always displayed first.
6289
=> _owningGroupIsDefault
63-
// Default group has the last index out of the Groups.Count
64-
// upper bound: so the DefaultGroup.Index == Groups.Count.
65-
? _owningListView.Groups.Count
66-
: _owningListView.Groups.IndexOf(_owningGroup);
90+
? 0
91+
// When calculating the index of other groups, we add a shift if the default group is displayed
92+
: _owningListViewAccessibilityObject.OwnerHasDefaultGroup
93+
? _owningListView.Groups.IndexOf(_owningGroup) + 1
94+
: _owningListView.Groups.IndexOf(_owningGroup);
6795

6896
public override string DefaultAction
6997
=> SR.AccessibleActionDoubleClick;
@@ -73,6 +101,8 @@ internal override UiaCore.ExpandCollapseState ExpandCollapseState
73101
? UiaCore.ExpandCollapseState.Collapsed
74102
: UiaCore.ExpandCollapseState.Expanded;
75103

104+
internal override UiaCore.IRawElementProviderFragmentRoot FragmentRoot => _owningListView.AccessibilityObject;
105+
76106
public override string Name
77107
=> _owningGroup.Header;
78108

@@ -131,7 +161,26 @@ private bool GetNativeFocus()
131161
return false;
132162
}
133163

134-
return LVGS.FOCUSED == unchecked((LVGS)(long)User32.SendMessageW(_owningListView, (User32.WM)LVM.GETGROUPSTATE, (IntPtr)CurrentIndex, (IntPtr)LVGS.FOCUSED));
164+
int nativeGroupId = GetNativeGroupId();
165+
if (nativeGroupId == -1)
166+
{
167+
return false;
168+
}
169+
170+
return LVGS.FOCUSED == unchecked((LVGS)(long)User32.SendMessageW(_owningListView, (User32.WM)LVM.GETGROUPSTATE, (IntPtr)nativeGroupId, (IntPtr)LVGS.FOCUSED));
171+
}
172+
173+
private unsafe int GetNativeGroupId()
174+
{
175+
LVGROUPW lvgroup = new LVGROUPW
176+
{
177+
cbSize = (uint)sizeof(LVGROUPW),
178+
mask = LVGF.GROUPID,
179+
};
180+
181+
return User32.SendMessageW(_owningListView, (User32.WM)LVM.GETGROUPINFOBYINDEX, (IntPtr)CurrentIndex, ref lvgroup) == IntPtr.Zero
182+
? -1
183+
: lvgroup.iGroupId;
135184
}
136185

137186
internal override object? GetPropertyValue(UiaCore.UIA propertyID)
@@ -157,6 +206,19 @@ private bool GetNativeFocus()
157206
internal IReadOnlyList<ListViewItem> GetVisibleItems()
158207
{
159208
List<ListViewItem> visibleItems = new();
209+
if (_owningGroupIsDefault)
210+
{
211+
foreach (ListViewItem? listViewItem in _owningListView.Items)
212+
{
213+
if (listViewItem is not null && listViewItem.Group is null)
214+
{
215+
visibleItems.Add(listViewItem);
216+
}
217+
}
218+
219+
return visibleItems;
220+
}
221+
160222
foreach (ListViewItem listViewItem in _owningGroup.Items)
161223
{
162224
if (listViewItem.ListView is not null)
@@ -213,26 +275,13 @@ internal IReadOnlyList<ListViewItem> GetVisibleItems()
213275
return null;
214276
}
215277

216-
if (!_owningGroupIsDefault)
278+
IReadOnlyList<ListViewItem> visibleItems = GetVisibleItems();
279+
if (index < 0 || index >= visibleItems.Count)
217280
{
218-
IReadOnlyList<ListViewItem> visibleItems = GetVisibleItems();
219-
if (index < 0 || index >= visibleItems.Count)
220-
{
221-
return null;
222-
}
223-
224-
return visibleItems[index].AccessibilityObject;
225-
}
226-
227-
foreach (ListViewItem? item in _owningListView.Items)
228-
{
229-
if (item is not null && item.Group is null && index-- == 0)
230-
{
231-
return item.AccessibilityObject;
232-
}
281+
return null;
233282
}
234283

235-
return null;
284+
return visibleItems[index].AccessibilityObject;
236285
}
237286

238287
private int GetChildIndex(AccessibleObject child)
@@ -300,23 +349,7 @@ public override int GetChildCount()
300349
return -1;
301350
}
302351

303-
if (_owningGroupIsDefault)
304-
{
305-
int count = 0;
306-
foreach (ListViewItem? item in _owningListView.Items)
307-
{
308-
if (item is not null && item.Group is null)
309-
{
310-
count++;
311-
}
312-
}
313-
314-
return count;
315-
}
316-
else
317-
{
318-
return GetVisibleItems().Count;
319-
}
352+
return GetVisibleItems().Count;
320353
}
321354

322355
internal override bool IsPatternSupported(UiaCore.UIA patternId)

src/System.Windows.Forms/src/System/Windows/Forms/ListViewItem.ListViewItemAccessibleObject.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,10 @@ internal override int[]? RuntimeId
179179
runtimeId[0] = owningListViewRuntimeId[0];
180180
runtimeId[1] = owningListViewRuntimeId[1];
181181
runtimeId[2] = 4; // Win32-control specific RuntimeID constant, is used in similar Win32 controls and is used in WinForms controls for consistency.
182-
runtimeId[3] = _owningListView.GetGroupIndex(_owningGroup);
182+
runtimeId[3] = _owningGroup.AccessibilityObject is ListViewGroupAccessibleObject listViewGroupAccessibleObject
183+
? listViewGroupAccessibleObject.CurrentIndex
184+
: -1;
185+
183186
runtimeId[4] = CurrentIndex;
184187

185188
return runtimeId;

src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ListVIew.ListViewAccessibleObjectTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ public static IEnumerable<object[]> ListViewAccessibleObject_OwnerHasDefaultGrou
424424
{
425425
foreach (bool createHandle in new[] { true, false })
426426
{
427-
bool expected = showGroups && createHandle;
427+
bool expected = showGroups;
428428
yield return new object[] { showGroups, createHandle, expected };
429429
}
430430
}

0 commit comments

Comments
 (0)