在WPF中使控件对鼠标交互透明

时间:2017-11-06 04:54:27

标签: c# wpf xaml listbox mouse

我正在尝试使WPF列表框复制旧Winforms CheckedListBox的行为,或者在例如中使用的复选列表框。 AnkhSVN的。我已经看到了一些示例,展示了如何使用DataTemplate每次创建一个复选框(例如Wpf CheckedListbox - how to get selected item),但与winforms控件相比,这感觉非常笨重:

  • 默认情况下,“如果用户更改了检查状态,请确保所有选定项目的检查状态更改”的逻辑。
  • 将项目从选中更改为未选中的匹配区域是框/和/标题,而不仅仅是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进行正确的选择处理。

非常感谢任何帮助。

1 个答案:

答案 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实现的确切行为之间引入的耦合:

  • 检查键盘状态
  • 如果用户开始在复选框内拖动
  • ,它将无法处理拖动
  • 它应该发生在mouseup上,而不是mousedown。但它足以满足我的需求。

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;
}