繁体   English   中英

单击内部的按钮时,如何不关闭xtk:SplitButton的自定义弹出窗口?

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

我有一个自定义控件,其中包含OnApplyTemplate的重写。 在其中,我尝试访问子模板的子模板,但似乎未加载它们。 我想什么:当该PART_IncreaseButton内侧Popup一个的xtk:SplitButton被点击, Popup不会接近,但只是让Button反应的点击。

CustomIntegerUpDownCustomSplitButton源自Xceed Extended WPF Toolkit。 CustomIntegerUpDown的样式,模板或代码均未更改,目前,其唯一目的是执行我在上面所说的内容,但我只是刚开始。 以下是所有相关来源。

我尝试了这个:

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

之后,IncrementButton为null,尽管在立即窗口中:

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

然后

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

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

VisualTreeHelper.GetChildrenCount(PartPopup)返回0

PartPopup.ApplyTemplate()返回false

我也看到了这一点 ,但不确定是否值得尝试这种方式。

FindChild是这个(从这里获取 ):

/// <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内部模板内的模板子级。 但是我没有找到一种方法。

我的相关问题在这里

更新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

我已经报告了这个问题

关键是Popup的内容不直接属于可视化树的一部分。 这就是为什么寻找Popup可视子代总是返回null Popup的内容是单独呈现的,并分配给Popup.Child属性。 您需要从Child属性中提取那些内容,然后继续在Popup内部进行树遍历。

以下是自定义可视树帮助器方法,用于返回与给定名称匹配的第一个子元素。 该帮助器可以在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;
  }
}

备注

Loaded父元素之后进行搜索非常重要。

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


编辑:单击内容后,保持Popup窗口打开

既然您已经告诉我您正在使用ToolBar SplitButton ,那么我立即知道问题的根源:

WPF中默认为焦点范围的类是WindowMenuItemToolBarContextMenu [Microsoft Docs: 逻辑焦点 ]

只需从ToolBar上删除焦点范围,以防止在单击其任何内容时将焦点从Popup窗口中删除(收到逻辑焦点):

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

编辑:请Popup开放上PART_ToggleButton当点击而Popup是开放的

为了防止在Popup 窗口打开时单击PART_ToggleButtonPopup窗口关闭并重新打开,您需要自行处理鼠标按下事件(应用程序范围)和打开Popup窗口。

首先修改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 ;
}

通过类型和名称查找可视父项的扩展方法

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM