【发布时间】:2019-01-13 03:07:31
【问题描述】:
我需要一个类似于 Viewbox 但拉伸规则略有不同的布局控件。我从 .NET 参考源中复制了 Viewbox 源,但现在我在子控件中看到了奇怪的数据绑定问题。
这是一个简化的例子:
<Window
x:Class="CustomViewbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomViewbox"
Title="MainWindow" Height="450" Width="800"
>
<local:Viewbox Stretch="Uniform">
<Grid>
<StackPanel>
<TextBlock Foreground="Red" Margin="5" Text="{Binding Path=Round,StringFormat={}Round {0}}"/>
<Button Margin="5" Content="Next round" Click="next_round_Click"/>
</StackPanel>
</Grid>
</local:Viewbox>
</Window>
以及对应的代码隐藏:
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
namespace CustomViewbox
{
public class Game : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
int round = 1;
public int Round { get { return round; } set { round = value; NotifyPropertyChanged(); } }
}
public partial class MainWindow : Window
{
Game game;
public MainWindow()
{
InitializeComponent();
game = new Game();
DataContext = game;
}
private void next_round_Click(object sender, RoutedEventArgs e)
{
++game.Round;
}
}
}
运行时,TextBlock 为空或不可见(未折叠)。到目前为止,我发现了 3 个不满意的解决方法:
- 如果我只用 Viewbox 替换 local:Viewbox,则布局错误
- 如果我从 TextBlock 中删除 Foreground 属性,则文本颜色错误
- 最后,如果我将属性 DataContext="{Binding}" 添加到 TextBlock,它看起来还可以,但这感觉不是一个正确的修复方法
有人可以解释发生了什么,或者如何调试吗?
为了完整起见,这里是修改后的 Viewbox 源代码,这应该是重现我的问题所需的所有代码:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace CustomViewbox
{
public class Viewbox : Decorator
{
private ContainerVisual _internalVisual;
public static readonly DependencyProperty StretchProperty = DependencyProperty.Register("Stretch", typeof(Stretch), typeof(Viewbox), new FrameworkPropertyMetadata(Stretch.Uniform, FrameworkPropertyMetadataOptions.AffectsMeasure), new ValidateValueCallback(ValidateStretchValue));
private static bool ValidateStretchValue(object value)
{
var s = (Stretch)value;
return s == Stretch.Uniform
|| s == Stretch.None
|| s == Stretch.Fill
|| s == Stretch.UniformToFill;
}
private ContainerVisual InternalVisual
{
get
{
if (_internalVisual == null)
{
_internalVisual = new ContainerVisual();
AddVisualChild(_internalVisual);
}
return _internalVisual;
}
}
private UIElement InternalChild
{
get
{
VisualCollection vc = InternalVisual.Children;
if (vc.Count != 0) return vc[0] as UIElement;
else return null;
}
set
{
VisualCollection vc = InternalVisual.Children;
if (vc.Count != 0) vc.Clear();
vc.Add(value);
}
}
private Transform InternalTransform
{
get
{
return InternalVisual.Transform;
}
set
{
InternalVisual.Transform = value;
}
}
public override UIElement Child
{
// everything is the same as on Decorator, the only difference is to insert intermediate Visual to specify scaling transform
get
{
return InternalChild;
}
set
{
UIElement old = InternalChild;
if (old != value)
{
//need to remove old element from logical tree
RemoveLogicalChild(old);
if (value != null)
{
AddLogicalChild(value);
}
InternalChild = value;
InvalidateMeasure();
}
}
}
protected override int VisualChildrenCount
{
get { return 1; /* Always have internal container visual */ }
}
protected override Visual GetVisualChild(int index)
{
if (index != 0)
{
throw new ArgumentOutOfRangeException("index", index, /*SR.Get(SRID.Visual_ArgumentOutOfRange)*/"argument out of range");
}
return InternalVisual;
}
public Stretch Stretch
{
get { return (Stretch)GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
/// <summary>
/// Updates DesiredSize of the Viewbox. Called by parent UIElement. This is the first pass of layout.
/// </summary>
/// <remarks>
/// Viewbox measures it's child at an infinite constraint; it allows the child to be however large it so desires.
/// The child's returned size will be used as it's natural size for scaling to Viewbox's size during Arrange.
/// </remarks>
/// <param name="constraint">Constraint size is an "upper limit" that the return value should not exceed.</param>
/// <returns>The Decorator's desired size.</returns>
protected override Size MeasureOverride(Size constraint)
{
var child = InternalChild;
var parentSize = new Size();
if (child != null)
{
// Initialize child constraint to infinity. We need to get a "natural" size for the child in absence of constraint.
// Note that an author *can* impose a constraint on a child by using Height/Width, &c... properties
var infiniteConstraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
child.Measure(infiniteConstraint);
var childSize = child.DesiredSize;
var scalefac = ComputeScaleFactor(constraint, childSize, Stretch);
parentSize.Width = scalefac * childSize.Width;
parentSize.Height = scalefac * childSize.Height;
if (parentSize.Width > constraint.Width)
parentSize.Width = constraint.Width;
childSize = new Size(constraint.Width / scalefac, constraint.Height / scalefac);
child.Measure(childSize);
}
return parentSize;
}
/// <summary>
/// Viewbox always sets the child to its desired size. It then computes and applies a transformation
/// from that size to the space available: Viewbox's own input size less child margin.
///
/// Viewbox also calls arrange on its child.
/// </summary>
/// <param name="arrangeSize">Size in which Border will draw the borders/background and children.</param>
protected override Size ArrangeOverride(Size arrangeSize)
{
var child = InternalChild;
if (child != null)
{
var childSize = child.DesiredSize;
// Compute scaling factors from arrange size and the measured child content size
var scalefac = ComputeScaleFactor(arrangeSize, childSize, Stretch);
InternalTransform = new ScaleTransform(scalefac, scalefac);
childSize = new Size(arrangeSize.Width / scalefac, arrangeSize.Height / scalefac);
// Arrange the child to the desired size
child.Arrange(new Rect(new Point(), childSize));
// return the size occupied by scaled child
arrangeSize.Width = scalefac * childSize.Width;
arrangeSize.Height = scalefac * childSize.Height;
}
return arrangeSize;
}
/// <summary>
/// This is a helper function that computes scale factors depending on a target size and a content size
/// </summary>
/// <param name="availableSize">Size into which the content is being fitted.</param>
/// <param name="contentSize">Size of the content, measured natively (unconstrained).</param>
/// <param name="stretch">Value of the Stretch property on the element.</param>
internal static double ComputeScaleFactor(Size availableSize, Size contentSize, Stretch stretch)
{
// Compute scaling factors to use for axes
var scale = 1.0;
var isConstrainedHeight = !Double.IsPositiveInfinity(availableSize.Height);
if (isConstrainedHeight)
{
// Compute scaling factors for both axes
scale = (DoubleUtil.IsZero(contentSize.Height)) ? 0.0 : availableSize.Height / contentSize.Height;
}
return scale;
}
}
static class DoubleUtil
{
internal const double DBL_EPSILON = 2.2204460492503131e-016; /* smallest such that 1.0+DBL_EPSILON != 1.0 */
internal const float FLT_MIN = 1.175494351e-38F; /* Number close to zero, where float.MinValue is -float.MaxValue */
public static bool IsZero(double value)
{
return Math.Abs(value) < 10.0 * DBL_EPSILON;
}
}
}
【问题讨论】:
-
我将您的代码与参考源进行了比较 - 一个显着的区别是您没有覆盖 LogicalChildren 属性。
-
这些“略有不同的拉伸规则”到底是什么?也许有更简单的方法来实现它们。
-
详细阐述克劳斯的观点。 DataContext 被标记为继承,因此沿逻辑树继承。覆盖 LogicalChildren 听起来可能有必要让该过程正常工作。 IIRC 任何孩子的 DP 也必须标记为 Inherits 才能使继承工作。我从来没有发现自己需要对视图框进行子类型化。常规视图框有什么“错误”?
-
我认为我需要一个自定义视图框,因为我想保留孩子所需的高度(因此它垂直适合窗口)但忽略其所需的宽度(我想要统一缩放,并且孩子宽度应该适应窗口纵横比)。
-
@KlausGütter 我删除了 LogicalChildren 覆盖,因为它是内部的。我不能在我自己的班级中覆盖它,对吧?
标签: c# wpf data-binding viewbox