我有一个带有OnApplyTemplate覆盖的自定义控件。在它中,我试图访问子模板的子模板,但它们似乎没有加载。我想要的是:当单击Popup
of a xtk:SplitButton
中的Popup
时,Popup
不会关闭,而只是让Button
对单击做出反应。
CustomIntegerUpDown
和CustomSplitButton
是从Xceed扩展的WPF工具包中派生的。CustomIntegerUpDown没有改变样式、模板或代码隐藏,目前它的唯一目的是执行我前面说过的话,但我只是刚刚开始。以下是所有相关的来源。
我试过这个:
IncrementButton = Utils.FindChild<RepeatButton>(PartPopup, "PART_IncreaseButton")
在此之后,IncrementButton是空的,尽管在直接窗口中:
Utils.FindChild<Popup>(this, "PART_Popup")
返回从GetTemplateChild("PART_Popup")
获得的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">
>
</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
扩展方法有两个版本:
不工作版本
有可能使这个版本以某种方式工作吗?我认为它更有效率。
/// <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
我已经报告了本期。
发布于 2019-09-08 18:47:03
关键是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
,FrameworkElement.IsLoaded
返回false
,您就必须订阅FrameworkElement.Loaded
事件。在此处理程序中,可以继续可视树遍历。
类似于Popup
的元素或折叠控件增加了可视树遍历的复杂性。
编辑:单击内容时保持Popup
打开
既然您已经告诉我,您正在ToolBar
中使用ToolBar
,我马上就知道了问题的根源:
默认情况下,WPF中属于焦点作用域的类是
Window
、MenuItem
、ToolBar
和ContextMenu
。[Microsoft:逻辑焦点]
只需从ToolBar
中删除焦点范围,以防止焦点在点击任何内容(接收到逻辑焦点)后立即从Popup
中移除:
<ToolBar FocusManager.IsFocusScope="False">
<CustomSplitButton />
</ToolBar>
编辑:当Popup
打开时,在PART_ToggleButton上单击时保持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 ;
}
扩展方法按类型和名称查找可视父级
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);
}
}
}
https://stackoverflow.com/questions/57842168
复制相似问题