【问题标题】:Making controls transparent to mouse interaction in WPF使控件对 WPF 中的鼠标交互透明
【发布时间】:2017-11-06 04:54:27
【问题描述】:

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

  • 默认不存在“如果用户更改检查状态,请确保所有选定项的检查状态更改”的逻辑。
  • 将项目从选中更改为未选中的点击区域是框/和/标题,而不仅仅是 Winforms 中的框

我可以通过在绑定集合中的每个项目上为 PropertyChanged 事件添加一个侦听器来处理第一个问题,如果 IsChecked 发生变化,则将 IsChecked 设置为所有当前选定项目的相同值。

但是,我找不到第二个问题的好的解决方案。通过将 DataTemplate 拆分为没有标题的 Checkbox 和带有标题的 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" 的各种组合,我确实设法让整个项目不响应鼠标点击事件 - 但我不能让它只有复选框响应鼠标事件,而其他所有内容都传递给 ListBox 以进行正确的选择处理。

任何帮助将不胜感激。

【问题讨论】:

  • 没有一个好的minimal reproducible example,试图让你的代码工作是不切实际的。但是,我认为如果您根本不弄乱IsHitTestVisible 属性,它应该可以正常工作。顺便说一句,RadioButton 开箱即用。你不需要你写的代码隐藏。只需确保要组合在一起的所有 RadioButton 元素都具有相同的 GroupName 值。
  • 您有如何使 RadioButton 工作的示例吗?因为如果我用单选按钮替换我的 StackPanel,我会得到一个完整的单选按钮列表,这些单选按钮不响应任何内容。但主要问题是单选按钮仍然对其文本上的鼠标事件不透明 - 它们拦截点击,而不是让 ListBox 处理它们。这是我不知道如何解决的主要问题,正如我的帖子的标题和文字所示。

标签: c# wpf xaml listbox mouse


【解决方案1】:

再次回答我自己的问题。

好吧,我找不到一个干净的方法来做到这一点,所以我最终将 ListBoxItem 设置为 IsHitTestVisible="False",并使用 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;
}

【讨论】:

    猜你喜欢
    • 2011-05-12
    • 2013-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-30
    • 2017-07-12
    • 1970-01-01
    相关资源
    最近更新 更多