如何在Border中精确居中渲染文本

时间:2015-03-05 14:01:14

标签: wpf xaml layout fonts

我有Border Content TextBlock我想要在水平和垂直方向上完美居中。无论我尝试什么,它都不会看起来居中。我错过了什么?

使用文本顶部下面的代码比边框低19px,文本底部高出边框5px。它也偏离中心左侧或右侧,具体取决于我假设与字体相关的Text值。

该解决方案适用于任何字体的不同文本(1-31)。

代码

<Grid Width="50" Height="50">
    <Border BorderThickness="1" BorderBrush="Black">
        <TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
    </Border>
</Grid>

结果

enter image description here enter image description here

3 个答案:

答案 0 :(得分:4)

那么,挑战被接受;-)这个解决方案基于以下想法:

  1. 将TextBlock放在边框内,并确保呈现整个文本,即使不可见。
  2. 将文本渲染为位图。
  3. 检测位图内的字形(即字符)以获得像素精确位置。
  4. 更新UI布局,使文本在边框内居中。
  5. 如果可能,请允许简单,通用的用法。
  6. 1。边框内的TextBlock /完全渲染

    一旦您意识到ScrollViewer的整个内容都已呈现,这很简单,所以这是我的UserControl XAML:

    <UserControl x:Class="WpfApplication4.CenteredText"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 mc:Ignorable="d" 
                 d:DesignHeight="300" d:DesignWidth="300">
        <Grid>
            <ScrollViewer x:Name="scroll" 
                          IsHitTestVisible="False"
                          VerticalScrollBarVisibility="Hidden"
                          HorizontalScrollBarVisibility="Hidden" />
        </Grid>
    </UserControl>
    

    将代码隐藏为:

    public partial class CenteredText : UserControl
    {
        public CenteredText()
        {
            InitializeComponent();
        }
    
        public static readonly DependencyProperty ElementProperty = DependencyProperty
            .Register("Element", typeof(FrameworkElement), typeof(CenteredText),
            new PropertyMetadata(OnElementChanged));
    
        private static void OnElementChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {
            var elem = e.NewValue as FrameworkElement;
            var ct = d as CenteredText;
            if(elem != null)
            {
                elem.Loaded += ct.Content_Loaded;
                ct.scroll.Content = elem;
            }
        }
    
        public FrameworkElement Element
        {
            get { return (FrameworkElement)GetValue(ElementProperty); }
            set { SetValue(ElementProperty, value); }
        }
    
        void Content_Loaded(object sender, RoutedEventArgs e) /*...*/
    }
    

    此控件基本上是ContentControl,允许一般处理内容的Loaded事件。可能有一种更简单的方法,我不确定。

    2。渲染到位图

    这个很简单。在Content_Loaded()方法中:

    void Content_Loaded(object sender, RoutedEventArgs e)
    {       
        FrameworkElement elem = sender as FrameworkElement;
        int w = (int)elem.ActualWidth;
        int h = (int)elem.ActualHeight;
        var rtb = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
        rtb.Render(elem);
    
        /* glyph detection ... */
     }
    

    3。检测字形

    这非常容易,因为默认情况下TextBlock以完全透明的背景渲染,我们只对边界矩形感兴趣。这是通过一个单独的方法完成的:

    bool TryFindGlyphs(BitmapSource src, out Rect rc)
    {
        int left = int.MaxValue;
        int toRight = -1;
        int top = int.MaxValue;
        int toBottom = -1;
    
        int w = src.PixelWidth;
        int h = src.PixelHeight;
        uint[] buf = new uint[w * h];
        src.CopyPixels(buf, w * sizeof(uint), 0);
        for (int y = 0; y < h; y++)
        {
            for (int x = 0; x < w; x++)
            {
                // background is assumed to be fully transparent, i.e. 0x00000000 in Pbgra
                if (buf[x + y * w] != 0)
                {
                    if (x < left) left = x;
                    if (x > toRight) toRight = x;
                    if (y < top) top = y;
                    if (y > toBottom) toBottom = y;
                }
            }
        }
    
        rc = new Rect(left, top, toRight - left, toBottom - top);
        return (toRight > left) && (toBottom > top);
    }
    

    上述方法试图找到不透明的最左边,最右边,最上面和最下面的像素,并将结果作为输出参数中的Rect返回。

    4。更新布局

    稍后将在Content_Loaded方法中执行此操作:

    void Content_Loaded(object sender, RoutedEventArgs e)
    {       
        /* render to bitmap ... */
    
        Rect rc;
        if (TryFindGlyphs(rtb, out rc))
        {
            if (rc.Height > this.scroll.ActualHeight || rc.Width > this.scroll.ActualWidth)
            {
                return; // todo: error handling
            }
            double desiredV = rc.Top - 0.5 * (this.scroll.ActualHeight - rc.Height);
            double desiredH = rc.Left - 0.5 * (this.scroll.ActualWidth - rc.Width);
    
            if (desiredV > 0)
            {
                this.scroll.ScrollToVerticalOffset(desiredV);
            }
            else
            {
                elem.Margin = new Thickness(elem.Margin.Left, elem.Margin.Top - desiredV, 
                    elem.Margin.Right, elem.Margin.Bottom);
            }
            if (desiredH > 0)
            {
                this.scroll.ScrollToHorizontalOffset(desiredH);
            }
            else
            {
                elem.Margin = new Thickness(elem.Margin.Left - desiredH, elem.Margin.Top, 
                    elem.Margin.Right, elem.Margin.Bottom);
            }
        }
    }
    

    使用以下策略更新此UI:

    • 在两个方向上计算边框和字形矩形之间的所需偏移量
    • 如果所需的偏移量为正,则表示文本需要向上移动(或在水平情况下向左移动),以便我们可以向下(向右)向下滚动所需的偏移量。
    • 如果所需偏移为负,则表示文本需要向下移动(或在水平情况下向右移动)。这不能通过滚动来完成,因为TextBlock左上角对齐(默认情况下),ScrollViewer仍位于初始(上/左)位置。但是有一个简单的解决方案:将所需的偏移量添加到Margin的{​​{1}}。

    5。简单用法

    TextBlock控件的使用方法如下:

    CenteredText

    结果

    对于边框尺寸150x150和FontSize 150:

    enter image description here

    对于边框尺寸150x150和FontSize 50:

    enter image description here

    对于边框尺寸50x50和FontSize 50:

    enter image description here

    注意:存在1像素错误,文本左侧的空间比右侧空间厚1个像素或更薄。与顶部/底部间距相同。如果边框具有均匀宽度并且渲染文本为奇数宽度(未提供子像素完美性,抱歉),则会发生这种情况。

    结论

    所提出的解决方案应该可以解决任何Font,FontSize和Text的1像素错误,并且易于使用。

    如果您还没有注意到,那么对于与<Border BorderBrush="Black" BorderThickness="1" Width="150" Height="150"> <local:CenteredText> <local:CenteredText.Element> <TextBlock Text="31" FontSize="150" /> </local:CenteredText.Element> </local:CenteredText> </Border> 控件的FrameworkElement属性一起使用的Elem进行了非常有限的假设。因此,这也适用于任何具有透明背景且需要(接近)完美居中的元素。

答案 1 :(得分:0)

您所谈论的内容与您使用的特定字体(以及该字体中的字符)有关。不同的字体将具有不同的基线,高度和其他属性。为了解决这个问题,只需使用Padding上的BorderMargin上的TextBlock即可让它适合您想要的地方:

<Grid Width="50" Height="50">
    <Border BorderThickness="1" BorderBrush="Black">
        <TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" 
            FontSize="50" Margin="0,0,3,14" />
    </Border>
</Grid>

注意:您还可以使用TextBlock.TextAlignment Property调整文本内容的水平对齐方式。

答案 2 :(得分:0)

我将此添加为评论,但我没有足够的声誉:P

它偏离中心,因为您为网格指定的高度和宽度(50x50)太小而不能容纳50的字体大小。要么将大小增加到100x100,要么将字体大小降低到更小。

通过这样做来证明它们将在中心完美对齐 - 在Visual Studio的某个地方查看此代码。您将看到这些文本块的数量完全重叠。

<Grid Height="100" Width="100">
    <Border BorderThickness="1" BorderBrush="Black" >
        <TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
    </Border>
    <Border BorderThickness="1" BorderBrush="Black" >
        <TextBlock Text="31" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
    </Border>
</Grid>

我希望这可以帮助你:)