如何在单击其中的按钮时不关闭 xtk:SplitButton 的自定义弹出窗口?

How to not close a xtk:SplitButton's custom Popup when a click is made on a button inside it?

我有一个自定义控件,它覆盖了 OnApplyTemplate。在其中我试图访问子模板的子模板,但它们似乎没有被加载。我希望的是:当 xtk:SplitButtonPopup 内的 PART_IncreaseButton 被点击时, Popup 不会关闭,只是让 Button 对点击做出反应.

CustomIntegerUpDownCustomSplitButton 派生自 Xceed Extended WPF Toolkit。 CustomIntegerUpDown 没有改变样式或模板或代码隐藏,目前它的唯一目的是做我上面说的,但我才刚刚开始。以下是所有相关来源。

我试过这个:

IncrementButton = Utils.FindChild<RepeatButton>(PartPopup, "PART_IncreaseButton")

之后 IncrementButton 为 null,尽管在 Immediate Window:

Utils.FindChild<Popup>(this, "PART_Popup") return 从 GetTemplateChild("PART_Popup") 获得的 Popup

然后

Utils.FindChild<ButtonSpinner>(PartPopup, "PART_Spinner") returns null.

Utils.FindChild<CustomIntegerUpDown>(PartPopup, "MyCustomIntegerUpDown") return null.

VisualTreeHelper.GetChildrenCount(PartPopup) returns 0.

PartPopup.ApplyTemplate() returns false.

我也看过 但我不确定是否值得尝试这种方式。

FindChild是这个(取自here):

/// <summary>
/// Finds a Child of a given item in the visual tree.
/// </summary>
/// <param name="parent">A direct parent of the queried item.</param>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="childName">x:Name or Name of child. </param>
/// <returns>The first parent item that matches the submitted type parameter.
/// If not matching item can be found,
/// a null parent is being returned.</returns>
public static T FindChild<T>(System.Windows.DependencyObject parent, string childName)
    where T : System.Windows.DependencyObject
{
    // Confirm parent and childName are valid.
    if (parent == null) return null;
    T foundChild = null;
    int childrenCount = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
    for (int i = 0; i < childrenCount; i++)
    {
        var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i);
        // If the child is not of the request child type child
        T childType = child as T;
        if (childType == null)
        {
            // recursively drill down the tree
            foundChild = FindChild<T>(child, childName);
            // If the child is found, break so we do not overwrite the found child.
            if (foundChild != null) break;
        }
        else if (!string.IsNullOrEmpty(childName))
        {
            var frameworkElement = child as System.Windows.FrameworkElement;
            // If the child's name is set for search
            if (frameworkElement != null && frameworkElement.Name == childName)
            {
                // if the child's name is of the request name
                foundChild = (T)child;
                break;
            }
        }
        else
        {
            // child element found.
            foundChild = (T)child;
            break;
        }
    }
    return foundChild;
}

CustomSplitButton.xaml.cs 我有这个:

internal Popup PartPopup;
internal Button PartButtonWith1, PartButtonWith5, PartButtonWith10, PartButtonWithCustom;
internal RepeatButton IncrementButton;
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    PartPopup = (Popup)GetTemplateChild("PART_Popup");
    PartButtonWith1 = (Button)GetTemplateChild("PART_ButtonWith1");
    PartButtonWith5 = (Button)GetTemplateChild("PART_ButtonWith5");
    PartButtonWith10 = (Button)GetTemplateChild("PART_ButtonWith10");
    PartButtonWithCustom = (Button)GetTemplateChild("PART_ButtonWithCustom");
    PartPopup.ApplyTemplate();
    IncrementButton = Utils.FindChild<RepeatButton>(PartPopup, "PART_IncreaseButton");
    if (PartPopup != null)
    {
        PartPopup.PreviewMouseDown += PART_Popup_PreviewMouseUp;
        PartPopup.PreviewMouseUp += PART_Popup_PreviewMouseUp;
    }
    if (PartButtonWith1 != null)
    {
        PartButtonWith1.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWith5 != null)
    {
        PartButtonWith5.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWith10 != null)
    {
        PartButtonWith10.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWithCustom != null)
    {
        PartButtonWithCustom.Click += BtnCustom_Click;
    }
}

视觉树是这样的:

CustomSplitButton的样式如下(xmlns:xtkThemes="clr-namespace:Xceed.Wpf.Toolkit.Themes;assembly=Xceed.Wpf.Toolkit"):

<Style x:Key="AddCountSplitButtonStyle" TargetType="{x:Type xtk:SplitButton}">
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="IsTabStop" Value="False"/>
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Background" Value="{DynamicResource {ComponentResourceKey ResourceId=ButtonNormalBackgroundKey, TypeInTargetAssembly={x:Type xtkThemes:ResourceKeys}}}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource {ComponentResourceKey ResourceId=ButtonNormalOuterBorderKey, TypeInTargetAssembly={x:Type xtkThemes:ResourceKeys}}}"/>
    <Setter Property="DropDownContentBackground">
        <Setter.Value>
            <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                <GradientStop Color="#FFF0F0F0" Offset="0"/>
                <GradientStop Color="#FFE5E5E5" Offset="1"/>
            </LinearGradientBrush>
        </Setter.Value>
    </Setter>
    <Setter Property="Padding" Value="3"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type xtk:SplitButton}">
                <Grid x:Name="MainGrid" SnapsToDevicePixels="True">
                    <xtk:ButtonChrome x:Name="ControlChrome" BorderThickness="0" Background="{TemplateBinding Background}" RenderEnabled="{TemplateBinding IsEnabled}">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <Button x:Name="PART_ActionButton" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="0" Padding="{TemplateBinding Padding}" Style="{x:Null}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
                                <Button.Template>
                                    <ControlTemplate TargetType="{x:Type Button}">
                                        <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}"/>
                                    </ControlTemplate>
                                </Button.Template>
                                <Grid>
                                    <xtk:ButtonChrome x:Name="ActionButtonChrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" RenderMouseOver="{Binding IsMouseOver, ElementName=PART_ActionButton}" RenderPressed="{Binding IsPressed, ElementName=PART_ActionButton}" RenderEnabled="{TemplateBinding IsEnabled}">
                                        <xtk:ButtonChrome.BorderThickness>
                                            <Binding ConverterParameter="2" Path="BorderThickness" RelativeSource="{RelativeSource TemplatedParent}">
                                                <Binding.Converter>
                                                    <xtk:ThicknessSideRemovalConverter/>
                                                </Binding.Converter>
                                            </Binding>
                                        </xtk:ButtonChrome.BorderThickness>
                                        <ContentPresenter x:Name="ActionButtonContent" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                                    </xtk:ButtonChrome>
                                </Grid>
                            </Button>
                            <ToggleButton x:Name="PART_ToggleButton" Grid.Column="1" IsChecked="{Binding IsOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
                                <ToggleButton.IsHitTestVisible>
                                    <Binding Path="IsOpen" RelativeSource="{RelativeSource TemplatedParent}">
                                        <Binding.Converter>
                                            <xtk:InverseBoolConverter/>
                                        </Binding.Converter>
                                    </Binding>
                                </ToggleButton.IsHitTestVisible>
                                <ToggleButton.Template>
                                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                                        <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}"/>
                                    </ControlTemplate>
                                </ToggleButton.Template>
                                <Grid>
                                    <xtk:ButtonChrome x:Name="ToggleButtonChrome" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="1,0" RenderMouseOver="{Binding IsMouseOver, ElementName=PART_ToggleButton}" RenderPressed="{Binding IsPressed, ElementName=PART_ToggleButton}" RenderChecked="{TemplateBinding IsOpen}" RenderEnabled="{TemplateBinding IsEnabled}">
                                        <Grid x:Name="arrowGlyph" IsHitTestVisible="False" Margin="4,3">
                                            <Path x:Name="Arrow" Data="M0,0L3,0 4.5,1.5 6,0 9,0 4.5,4.5z" Fill="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" Height="5" Margin="0,1,0,0" Width="9"/>
                                        </Grid>
                                    </xtk:ButtonChrome>
                                </Grid>
                            </ToggleButton>
                        </Grid>
                    </xtk:ButtonChrome>
                    <Popup x:Name="PART_Popup" AllowsTransparency="True" Focusable="False" HorizontalOffset="1" IsOpen="{Binding IsChecked, ElementName=PART_ToggleButton}" Placement="{TemplateBinding DropDownPosition}" VerticalOffset="1"
                                StaysOpen="False">

                        <Border BorderThickness="{DynamicResource DefaultBorderThickness}" Margin="10,0,10,10" Background="{DynamicResource DarkerBaseBrush}" BorderBrush="{DynamicResource PopupBorderBrush}" CornerRadius="{DynamicResource DefaultCornerRadius}">
                            <Grid MinWidth="100" Name="PART_ContentPresenter">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition Height="Auto"/>
                                </Grid.RowDefinitions>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="Auto"/>
                                </Grid.ColumnDefinitions>
                                <Button x:Name="PART_ButtonWith1" Grid.Row="0" Grid.ColumnSpan="2">
                                    1
                                </Button>
                                <Button x:Name="PART_ButtonWith5" Grid.Row="1" Grid.ColumnSpan="2">
                                    5
                                </Button>
                                <Button x:Name="PART_ButtonWith10" Grid.Row="2" Grid.ColumnSpan="2">
                                    10
                                </Button>
                                <local:CustomIntegerUpDown Grid.Row="3" Value="1"
                                                            Increment="1" ClipValueToMinMax="True"              
                                                            x:Name="MyCustomIntegerUpDown">
                                </local:CustomIntegerUpDown>
                                <Button x:Name="PART_ButtonWithCustom" Grid.Row="3" Grid.Column="1" Padding="2,2,2,2">
                                    &gt;
                                </Button>
                            </Grid>
                            <Border.Effect>
                                <DropShadowEffect ShadowDepth="0" BlurRadius="10" Color="{DynamicResource Base6Color}" />
                            </Border.Effect>
                        </Border>
                    </Popup>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter Property="Fill" TargetName="Arrow" Value="#FFAFAFAF"/>
                        <Setter Property="Foreground" TargetName="ActionButtonChrome" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

OnApplyTemplate 中,我希望能够访问 this 中模板中的子模板。但是我没有找到办法做到这一点。

我的相关问题here

更新 1

示例的起点,已更新(它使用 BionicCode 的答案中的 TryFindVisualChildElementByName 扩展方法):

internal Popup PartPopup;
internal Button PartButtonWith1, PartButtonWith5, PartButtonWith10, PartButtonWithCustom;
internal RepeatButton IncrementButton;

private void SplitButton_Loaded(object sender, RoutedEventArgs e)
{
    PartPopup = (Popup)GetTemplateChild("PART_Popup");
    PartButtonWith1 = (Button)GetTemplateChild("PART_ButtonWith1");
    PartButtonWith5 = (Button)GetTemplateChild("PART_ButtonWith5");
    PartButtonWith10 = (Button)GetTemplateChild("PART_ButtonWith10");
    PartButtonWithCustom = (Button)GetTemplateChild("PART_ButtonWithCustom");

    if (PartPopup != null)
    {
        PartPopup.ApplyTemplateRecursively();

        if (PartPopup.TryFindVisualChildElementByName("PART_IncreaseButton", out FrameworkElement incButton))
        {
            IncrementButton = (RepeatButton)incButton;

            // do something with IncrementButton here
        }

        PartPopup.PreviewMouseDown += PART_Popup_PreviewMouseUp;
        PartPopup.PreviewMouseUp += PART_Popup_PreviewMouseUp;
    }

    if (PartButtonWith1 != null)
    {
        PartButtonWith1.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWith5 != null)
    {
        PartButtonWith5.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWith10 != null)
    {
        PartButtonWith10.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWithCustom != null)
    {
        PartButtonWithCustom.Click += BtnCustom_Click;
    }
}

上面使用的ApplyTemplateRecursively扩展方法,有2个版本:

无效版本

有没有可能让这个版本以某种方式工作?我觉得效率更高

/// <summary>
/// Not working because the ApplyTemplate affects the VisualTree and when applying
/// templates recursively it does not see the correct updated visual tree to be able
/// to continue.
/// </summary>
/// <param name="root"></param>
internal static void ApplyTemplateRecursively(this System.Windows.DependencyObject root)
{
    if (root is System.Windows.Controls.Primitives.Popup p)
    {
        p.Child.ApplyTemplateRecursively();
        return;
    }

    if (root is FrameworkElement r)
    {
        r.ApplyTemplate();
    }

    foreach (object element in System.Windows.LogicalTreeHelper.GetChildren(root))
    {
        if (element is System.Windows.DependencyObject el)
        {
            ApplyTemplateRecursively(el);
        }
    }
}

工作版本

/// <summary>
/// I am not sure if this is sufficiently efficient, because it goes through the entire visual tree.
/// </summary>
/// <param name="root"></param>
internal static void ApplyTemplateRecursively(this System.Windows.DependencyObject root)
{
    if (root is System.Windows.Controls.Primitives.Popup p)
    {
        p.Child.ApplyTemplateRecursively();
        return;
    }

    if (root is FrameworkElement r)
    {
        r.ApplyTemplate();
    }

    for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(root); ++i)
    {
        DependencyObject d = VisualTreeHelper.GetChild(root, i);
        ApplyTemplateRecursively(d);
    }
}

现在我正在尝试解决实际问题。

更新 2

我已举报this issue

重点是 Popup 的内容不直接是可视化树的一部分。这就是为什么寻找 Popup 的视觉 children 总是 return null。一个Popup的内容单独渲染,赋值给Popup.Child属性。在继续 Popup 中的树遍历之前,您需要从 Child 属性 中提取它们。

以下是 return 与给定名称匹配的第一个 child 元素的自定义可视树帮助器方法。这个助手在 Popup 元素中正确搜索。此方法是 DependencyObject 类型的扩展方法,必须放入 static class:

public static bool TryFindVisualChildElementByName(
  this DependencyObject parent,
  string childElementName,
  out FrameworkElement resultElement)
{
  resultElement = null;

  if (parent is Popup popup)
  {
    parent = popup.Child;
    if (parent == null)
    {
      return false;
    }
  }

  for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
  {
    DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);        

    if (childElement is FrameworkElement uiElement && uiElement.Name.Equals(
          childElementName,
          StringComparison.OrdinalIgnoreCase))
    {
      resultElement = uiElement;
      return true;
    }

    if (childElement.TryFindVisualChildElementByName(childElementName, out resultElement))
    {
      return true;
    }
  }

  return false;
}

这是一个扩展方法,它的用法是这样的:

CustomSplitButton.xaml.cs

// Constructor
public CustomSplitButton()
{
  this.Loaded += GetParts;
}

private void GetParts(object sender, RoutedEventArgs e)
{
  if (this.TryFindVisualChildElementByName("PART_Popup", out FrameworkElement popupPart))
  {
    if (popupPart.TryFindVisualChildElementByName("PART_ContentPresenter", out FrameworkElement contentPresenter))
    {
      if (!contentPresenter.IsLoaded)
      {
        contentPresenter.Loaded += CompleteSearch;
      }
      else 
      {
        CompleteSearch(contentPresenter, null);
      }
    }
  }
}

private void CompleteSearch(object sender, RoutedEventArgs e)
{      
  contentPresenter.Loaded -= CompleteSearch;

  if ((sender as DependencyObject).TryFindVisualChildElementByName("PART_IncreaseButton", out FrameworkElement increaseButton))
  {        
    IncrementButton = (RepeatButton) increaseButton;
  }
}

备注

在 一个 parent 元素是 Loaded 之后搜索 非常重要。

这适用于可视化树中的所有元素。由于 SplitButton 包含一个默认折叠的下拉列表,因此并非所有内容都在最初加载。打开下拉列表后,SplitButton 使其内容可见,这会将它们添加到可视化树中。到目前为止,SplitButton.IsLoaded 属性 将 return false,表示按钮的不完整视觉状态。你需要做的是,一旦遇到 FrameworkElement where FrameworkElement.IsLoaded returns false 你必须订阅 FrameworkElement.Loaded 事件。在此处理程序中,您可以继续可视化树遍历。
Popup 类似的元素或折叠的控件增加了视觉树遍历的复杂性。


编辑:单击内容时保持Popup打开

既然你已经告诉我你在 ToolBar 中使用 SplitButton,我马上就知道你问题的根源了:

Classes in WPF which are focus scopes by default are Window, MenuItem, ToolBar, and ContextMenu. [Microsoft Docs: Logical Focus]

只需从 ToolBar 中删除焦点范围,以防止在单击其任何内容(收到逻辑焦点)后立即从 Popup 中删除焦点:

<ToolBar FocusManager.IsFocusScope="False"> 
  <CustomSplitButton />
</ToolBar>

编辑:在 Popup 打开

时,单击 PART_ToggleButton 时保持 Popup 打开

要防止 PopupPopup 打开时单击 PART_ToggleButton 时关闭和重新打开,您需要处理鼠标按下事件(应用程序范围内)和自己打开弹出窗口。

首先修改 PART_Popup 使其保持打开状态,并从 IsOpen 属性:

中删除绑定

CustomSplitButton.xaml

<Popup x:Name="PART_Popup"
       IsOpen="False"
       StaysOpen="True"
       AllowsTransparency="True"
       Focusable="False"
       HorizontalOffset="1"
       Placement="{TemplateBinding DropDownPosition}"
       VerticalOffset="1">

然后在您的 CustomSplitButton 中观察鼠标设备的鼠标按下事件并确定命中目标。我假设您检索了基础 PART_PopupPART_ToggleButton 元素并将其存储在名为 属性 的PartPopupPartToggleButton(请参阅本答案的第一部分了解如何操作):

CustomSplitButton.xaml.cs

public CustomSplitButton()
{
  this.Loaded += OnLoaded;
}

private void OnLoaded(object sender, RoutedEventArgs e)
{
  Mouse.AddPreviewMouseDownHandler(Application.Current.MainWindow, KeepPopupOpen);
}

private void KeepPopupOpen(object sender, RoutedEventArgs routedEventArgs)
{
  var mouseClickSourceElement = routedEventArgs.OriginalSource as DependencyObject;
  var isPopupContentClicked = false;
  var isPartToggleButtonClicked = 
    object.ReferenceEquals(routedEventArgs.Source, this) 
      && mouseClickSourceElement.TryFindVisualParentElement(out ButtonBase button) 
      && button.Name.Equals(this.PartToggleButton.Name, StringComparison.OrdinalIgnoreCase);

  if (!isPartToggleButtonClicked)
  {
    isPopupContentClicked = 
      object.ReferenceEquals(routedEventArgs.Source, this) 
        && mouseClickSourceElement.TryFindVisualParentElementByName("PART_ContentPresenter", out FrameworkElement popupContentPresenter));
  }

  this.PartPopup.IsOpen = this.IsOpen = isPartToggleButtonClicked || isPopupContentClicked ;
}

扩展方法 按类型和名称查找视觉对象 parent

public static class HelperExtensions
{
  public static bool TryFindVisualParentElement<TParent>(this DependencyObject child, out TParent resultElement)
    where TParent : DependencyObject
  {
    resultElement = null;

    if (child == null)
    {
      return false;
    }

    DependencyObject parentElement = VisualTreeHelper.GetParent(child);

    if (parentElement is TParent parent)
    {
      resultElement = parent;
      return true;
    }

    return parentElement.TryFindVisualParentElement(out resultElement);
  }

  public static bool TryFindVisualParentElementByName(
      this DependencyObject child,
      string elementName,
      out FrameworkElement resultElement)
    {
      resultElement = null;

      if (child == null)
      {
        return false;
      }

      DependencyObject parentElement = VisualTreeHelper.GetParent(child);

      if (parentElement is FrameworkElement frameworkElement &&
          frameworkElement.Name.Equals(elementName, StringComparison.OrdinalIgnoreCase))
      {
        resultElement = frameworkElement;
        return true;
      }

      return parentElement.TryFindVisualParentElementByName(elementName, out resultElement);
    }
  }
}