如何在 Border 内准确居中渲染文本

How to exactly center rendered text inside a Border

我有一个 Border,其中 ContentTextBlock,我希望它在水平和垂直方向上都完美居中。无论我尝试什么,它看起来都不会居中。我错过了什么?

使用下面的代码,文本顶部在边框下方 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>

结果

您所说的与您使用的特定字体(以及该字体中的字符)有关。不同的字体会有不同的基线、高度和其他属性。为了解决这个问题,只需在 Border 上使用 Padding 或在 TextBlock 上使用 Margin 使其适合您想要的位置:

<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调整文本内容的水平对齐方式。

我想将其添加为评论,但我没有足够的声誉 :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>

希望对您有所帮助:)

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

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

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);
}

上述方法试图找到不透明的最左边、最右边、最上面和最下面的像素,并且 returns 结果作为输出参数中的 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 仍处于初始 (top/left) 位置。不过有一个简单的解决方案:将所需的偏移量添加到 TextBlockMargin

5。简单用法

CenteredText控件使用方法如下:

<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>

结果

对于边框大小 150x150 和字体大小 150:

对于边框大小 150x150 和 FontSize 50:

对于边框大小 50x50 和 FontSize 50:

注:文本左侧space比space粗或细1个像素存在1个像素错误向右。与顶部/底部间距相同。如果边框的宽度为偶数而呈现的文本宽度为奇数(抱歉,没有提供亚像素完美性),就会发生这种情况

结论

所提供的解决方案对于任何 Font、FontSize 和 Text 都应该能达到 1 个像素的错误,并且易于使用。

如果您还没有注意到,我们对与 CenteredText 控件的 Elem 属性 一起使用的 FrameworkElement 做出了非常有限的假设。因此,这也适用于任何具有透明背景并需要(接近)完美居中的元素。