![](/img/trans.png)
[英]Button click event is not fired on first click when place inside popup
[英]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
反应的点击。
CustomIntegerUpDown
和CustomSplitButton
源自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">
>
</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
内部模板内的模板子级。 但是我没有找到一种方法。
我的相关问题在这里 。
示例的起点已更新(它使用了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);
}
}
现在,我正在尝试解决实际问题。
我已经报告了这个问题 。
关键是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
返回false
的FrameworkElement
,则必须订阅FrameworkElement.Loaded
事件。 在此处理程序中,您可以继续可视树遍历。
Popup
式元素或折叠控件会增加可视化树遍历的复杂性。
Popup
窗口打开 既然您已经告诉我您正在使用ToolBar
SplitButton
,那么我立即知道问题的根源:
WPF中默认为焦点范围的类是
Window
,MenuItem
,ToolBar
和ContextMenu
。 [Microsoft Docs: 逻辑焦点 ]
只需从ToolBar
上删除焦点范围,以防止在单击其任何内容时将焦点从Popup
窗口中删除(收到逻辑焦点):
<ToolBar FocusManager.IsFocusScope="False">
<CustomSplitButton />
</ToolBar>
Popup
开放上PART_ToggleButton当点击而Popup
是开放的 为了防止在Popup
窗口打开时单击PART_ToggleButton时Popup
窗口关闭并重新打开,您需要自行处理鼠标按下事件(应用程序范围)和打开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_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);
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.