如何在单击其中的按钮时不关闭 xtk:SplitButton 的自定义弹出窗口?
How to not close a xtk:SplitButton's custom Popup when a click is made on a button inside it?
我有一个自定义控件,它覆盖了 OnApplyTemplate。在其中我试图访问子模板的子模板,但它们似乎没有被加载。我希望的是:当 xtk:SplitButton
的 Popup
内的 PART_IncreaseButton
被点击时, Popup
不会关闭,只是让 Button
对点击做出反应.
CustomIntegerUpDown
和 CustomSplitButton
派生自 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">
>
</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
打开
要防止 Popup
在 Popup
打开时单击 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_Popup 和 PART_ToggleButton 元素并将其存储在名为 属性 的PartPopup
和 PartToggleButton
(请参阅本答案的第一部分了解如何操作):
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);
}
}
}
我有一个自定义控件,它覆盖了 OnApplyTemplate。在其中我试图访问子模板的子模板,但它们似乎没有被加载。我希望的是:当 xtk:SplitButton
的 Popup
内的 PART_IncreaseButton
被点击时, Popup
不会关闭,只是让 Button
对点击做出反应.
CustomIntegerUpDown
和 CustomSplitButton
派生自 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">
>
</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
, andContextMenu
. [Microsoft Docs: Logical Focus]
只需从 ToolBar
中删除焦点范围,以防止在单击其任何内容(收到逻辑焦点)后立即从 Popup
中删除焦点:
<ToolBar FocusManager.IsFocusScope="False">
<CustomSplitButton />
</ToolBar>
编辑:在 Popup
打开
时,单击 PART_ToggleButton 时保持 Popup
打开
要防止 Popup
在 Popup
打开时单击 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_Popup 和 PART_ToggleButton 元素并将其存储在名为 属性 的PartPopup
和 PartToggleButton
(请参阅本答案的第一部分了解如何操作):
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);
}
}
}