我正在尝试使WPF列表框复制旧Winforms CheckedListBox的行为,或者在例如中使用的复选列表框。 AnkhSVN的。我已经看到了一些示例,展示了如何使用DataTemplate每次创建一个复选框(例如Wpf CheckedListbox - how to get selected item),但与winforms控件相比,这感觉非常笨重:
我可以通过在绑定集合中的每个项目上为PropertyChanged事件添加一个侦听器来处理第一个问题,如果IsChecked发生更改,则将IsChecked设置为所有当前所选项目的相同值。
但是,我找不到第二个问题的好方法。通过将DataTemplate拆分为没有标题的复选框和带有标题的TextBlock,我可以减少命中区域以将检查状态更改为仅所需的方格。然而,击中TextBlock的所有鼠标交互都没有做任何事情 - 我希望它的行为与普通列表框中的行为相同,或者在Textblock之外的死区中:如果用户持有shift,则选择所有内容,包括此项目,如果没有,则清除选择并仅选择此项目。我可以尝试实现我在TextBlock上处理Mouse *事件的东西,但这看起来很脆弱且不优雅 - 我试图重新创建ListBox的确切行为,而不是将事件传递到列表框。
这是我目前所拥有的:
XAML:
<ListBox x:Name="_lstReceivers" SelectionMode="Extended" Margin="10,41,6,15"
ItemsSource="{Binding Receivers}">
<ListBox.ItemTemplate>
<DataTemplate>
<ListBoxItem>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked}" IsHitTestVisible="True"/>
<TextBlock Text="{Binding Item}" Background="{x:Null}" IsHitTestVisible="False"/><!--Attempt to make it pass mouse events through. Doesn't work. Yuk.-->
</StackPanel>
</ListBoxItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
代码隐藏以获得“同时更改所有检查”逻辑(为清晰起见,删除了一些错误处理):
private void ListBoxItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var item = sender as CheckableItem<Receiver>;
if (item == null)
return;
if (e.PropertyName == nameof(CheckableItem<Receiver>.IsChecked))
{
bool newVal = item.IsChecked;
foreach (CheckableItem<Receiver> changeItem in _lstReceivers.SelectedItems)
{
changeItem.IsChecked = newVal;
}
}
}
通过尝试Background =“{x:Null}”和IsHitTestVisible =“False”的各种组合,我确实设法让整个项目无法响应鼠标点击事件 - 但我无法让它只有Checkbox响应鼠标事件,而其他所有事件都传递给ListBox进行正确的选择处理。
非常感谢任何帮助。
答案 0 :(得分:1)
再次回答我自己的问题。
好吧,我找不到干净的方法,所以我最终将ListBoxItem设置为IsHitTestVisible =&#34; False&#34;,并使用PreviewMouseDown手动跟踪鼠标事件。
最终代码:
XAML:
<ListBox x:Name="_lstReceivers" SelectionMode="Extended" Margin="10,41,6,15"
ItemsSource="{Binding Receivers}" PreviewMouseDown="_lstReceivers_MouseDown">
<ListBox.ItemTemplate>
<DataTemplate>
<ListBoxItem IsSelected="{Binding IsSelected}" IsHitTestVisible="False">
<StackPanel Orientation="Horizontal" Background="{x:Null}">
<CheckBox IsChecked="{Binding IsChecked}" IsHitTestVisible="True" Checked="CheckBox_Checked" Unchecked="CheckBox_Checked"/>
<TextBlock Text="{Binding Item}" Background="{x:Null}" IsHitTestVisible="False"/>
</StackPanel>
</ListBoxItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
代码背后:
//Logic to handle allowing the user to click the checkbox, but have everywhere else respond to normal listbox logic.
private void _lstReceivers_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
Visual curControl = _lstReceivers as Visual;
ListBoxItem testItem = null;
//Allow normal selection logic to take place if the user is holding shift or ctrl
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
return;
//Find the control which the user clicked on. We require the relevant ListBoxItem too, so we can't use VisualTreeHelper.HitTest (Or it wouldn't be much use)
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(curControl); i++)
{
var testControl = (Visual)VisualTreeHelper.GetChild(curControl, i);
var rect = VisualTreeHelper.GetDescendantBounds(testControl);
var pos = e.GetPosition((IInputElement)curControl) - VisualTreeHelper.GetOffset(testControl);
if (!rect.Contains(pos))
continue;
else
{
//There are multiple ListBoxItems in the tree we walk. Only take the first - and use it to remember the IsSelected property.
if (testItem == null && testControl is ListBoxItem)
testItem = testControl as ListBoxItem;
//If we hit a checkbox, handle it here
if (testControl is CheckBox)
{
//If the user has hit the checkbox of an unselected item, then only change the item they have hit.
if (!testItem.IsSelected)
dontChangeChecks++;
((CheckBox)testControl).IsChecked = !((CheckBox)testControl).IsChecked;
//If the user has hit the checkbox of a selected item, ensure that the entire selection is maintained (prevent normal selection logic).
if (testItem.IsSelected)
e.Handled = true;
else
dontChangeChecks--;
return;
}
//Like recursion, but cheaper:
curControl = testControl;
i = -1;
}
}
}
//Guard variable
int dontChangeChecks = 0;
//Logic to have all selected listbox items change at the same time
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
if (dontChangeChecks > 0)
return;
var newVal = ((CheckBox)sender).IsChecked;
dontChangeChecks++;
try
{
//This could be improved by making it more generic.
foreach (CheckableItem<Receiver> item in _lstReceivers.SelectedItems)
{
item.IsChecked = newVal.Value;
}
}
finally
{
dontChangeChecks--;
}
}
这个解决方案有效,但我不喜欢它在我的代码和ListBox实现的确切行为之间引入的耦合:
PS:绑定类,即使它不相关且显而易见:
public class CheckableItem<T> : INotifyPropertyChanged
{
public T Item { get; set; }
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value)
return;
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
private bool _checked;
public bool IsChecked
{
get => _checked;
set
{
if (_checked == value)
return;
_checked = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsChecked)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}