如何阻止ControlTemplate内的TextBlock随控件宽度调整而缩放?
首先得搞清楚为什么会出现这种奇怪的差异:你说调整宽度时标签跟着缩放,调整高度却不会,大概率是你的宽度调整逻辑用了ScaleTransform(缩放变换)来放大/缩小整个控件,而高度调整是直接修改控件的Height属性(只是改变尺寸,没有应用缩放)。这样一来,宽度缩放时,ControlTemplate里的整个Grid(包括标签)都会被缩放,标签的Margin、文字大小跟着变大变小;而高度调整时,只是控件的高度变了,标签的布局属性(比如Margin)是固定像素值,所以位置会变但大小不受影响。
要让标签完全不受缩放影响,同时还能跟着控件移动,最靠谱的办法是把标签从被缩放的布局层级里抽出来,放到AdornerLayer中——Adorner是独立于控件本身的渲染层,只会跟随控件的位置,不会受控件的缩放变换影响。下面是具体的实现步骤:
步骤1:创建自定义Adorner类来承载标签
这个类会负责渲染标签,以及处理删除按钮的逻辑:
using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Media.Imaging; public class DesignerItemLabelAdorner : Adorner { private readonly Border _labelBorder; private readonly ContentControl _targetItem; public DesignerItemLabelAdorner(ContentControl adornedElement) : base(adornedElement) { _targetItem = adornedElement; IsHitTestVisible = true; // 确保按钮可以点击 // 构建标签的UI结构,和你原来的模板一致 _labelBorder = new Border { Background = Brushes.LightGray, BorderBrush = Brushes.Black, BorderThickness = new Thickness(2), CornerRadius = new CornerRadius(3), Margin = new Thickness(5, -35, 0, 0) }; var stackPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(5) }; // 绑定标签文字到控件的Name属性 var textBlock = new TextBlock { Foreground = Brushes.Black }; textBlock.SetBinding(TextBlock.TextProperty, new Binding("Name") { Source = _targetItem, Mode = BindingMode.TwoWay }); // 创建删除按钮,样式和原来一致 var deleteBtn = new Button { Height = 15, MinWidth = 15, Width = 15, Margin = new Thickness(5, 0, 0, 0), Background = Brushes.LightGray }; deleteBtn.Click += (s, e) => DeleteTargetItem(); var deleteImg = new Image { Height = 7, Width = 7, Source = new BitmapImage(new Uri("/Resources/Images/cross_48.png", UriKind.Relative)) }; deleteBtn.Content = deleteImg; // 设置按钮的样式触发器 var btnStyle = new Style(typeof(Button)); btnStyle.Setters.Add(new Setter(Button.BackgroundProperty, Brushes.LightGray)); btnStyle.Setters.Add(new Setter(Button.TemplateProperty, new ControlTemplate(typeof(Button)) { VisualTree = new FrameworkElementFactory(typeof(Border)) { Bindings = { new Binding("Background") { Source = deleteBtn } }, Child = new FrameworkElementFactory(typeof(ContentPresenter)) { Setters = { new Setter(ContentPresenter.FocusableProperty, false), new Setter(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center), new Setter(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center) } } } })); btnStyle.Triggers.Add(new Trigger { Property = Button.IsMouseOverProperty, Value = true, Setters = { new Setter(Button.BackgroundProperty, Brushes.LightGray) } }); btnStyle.Triggers.Add(new Trigger { Property = Button.IsPressedProperty, Value = true, Setters = { new Setter(Button.BackgroundProperty, Brushes.DarkGray) } }); btnStyle.Triggers.Add(new Trigger { Property = Button.IsEnabledProperty, Value = false, Setters = { new Setter(UIElement.OpacityProperty, 0.35) } }); deleteBtn.Style = btnStyle; stackPanel.Children.Add(textBlock); stackPanel.Children.Add(deleteBtn); _labelBorder.Child = stackPanel; AddVisualChild(_labelBorder); } private void DeleteTargetItem() { // 复用你原来的删除逻辑 if (_targetItem.Parent is Panel parentPanel) { parentPanel.Children.Remove(_targetItem); } } // 必须重写的Adorner方法 protected override int VisualChildrenCount => 1; protected override Visual GetVisualChild(int index) => index == 0 ? _labelBorder : throw new ArgumentOutOfRangeException(nameof(index)); protected override Size MeasureOverride(Size constraint) { _labelBorder.Measure(constraint); return _labelBorder.DesiredSize; } protected override Size ArrangeOverride(Size finalSize) { // 把标签定位到控件的左上角上方 _labelBorder.Arrange(new Rect(new Point(0, -_labelBorder.DesiredSize.Height), _labelBorder.DesiredSize)); return finalSize; } }
步骤2:修改ControlTemplate,移除原来的标签部分
把你原来模板里的BoxLabelBorder及其子元素全部删掉,简化后的模板如下:
<!-- Designer Item Template--> <ControlTemplate x:Key="DesignerItemTemplate" TargetType="ContentControl"> <Grid x:Name="DesignerGrid" DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}"> <s:MoveThumb x:Name="MoveThumbAdorner" Template="{StaticResource MoveThumbTemplate}" Cursor="SizeAll"/> <Control Template="{StaticResource ResizeDecoratorTemplate}"/> <ContentPresenter x:Name="rectCP" Content="{TemplateBinding ContentControl.Content}"/> </Grid> </ControlTemplate>
步骤3:在DesignerItem加载时添加Adorner
给你的ContentControl(DesignerItem)添加Loaded事件,在事件里把Adorner绑定到控件上:
private void DesignerItem_Loaded(object sender, RoutedEventArgs e) { if (sender is ContentControl designerItem) { var adornerLayer = AdornerLayer.GetAdornerLayer(designerItem); adornerLayer?.Add(new DesignerItemLabelAdorner(designerItem)); } }
别忘了在XAML里给DesignerItem绑定这个事件:
<ContentControl Template="{StaticResource DesignerItemTemplate}" Loaded="DesignerItem_Loaded" .../>
这样处理之后,标签会始终附着在控件的左上角上方,不管你怎么缩放控件的宽度或高度,标签的大小都不会变,而且会跟着控件一起移动。
另外,如果你不想用Adorner,也可以把标签作为Canvas的独立子元素,通过绑定控件的Canvas.Left和Canvas.Top属性来同步位置——但这种方法需要你的控件都放在Canvas布局里,灵活性不如Adorner高。
内容的提问来源于stack exchange,提问作者nwahs

