wpf - 如何将渲染文本精确居中在边框内

标签 wpf xaml layout fonts

我有一个 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>

结果

enter image description here enter image description here

最佳答案

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

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

上述方法尝试查找最左、最右、最顶部和最底部不透明的像素,并在输出参数中以 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 仍处于初始(上/左)位置。不过,有一个简单的解决方案:将所需的偏移量添加到 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:

enter image description here

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

enter image description here

对于边框大小 50x50 和字体大小 50:

enter image description here

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

结论

所提出的解决方案对于任何字体、字体大小和文本都应达到 1 像素的误差,并且易于使用。

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

关于wpf - 如何将渲染文本精确居中在边框内,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28879606/

相关文章:

xaml - 使用资源定义厚度

html - HTML/CSS 如何确定大小?

html - 当内部 div 包含开始换行的文本时,为什么将 div 元素推到 anchor 下方?

c# - 在 WPF DataGrid 的单个单元格上设置删除线的最佳方法?

c# - ICommand MVVM 实现

WPF Xaml 处理顺序

java - 如何在 jbutton 单击时将 jlabel 添加到另一个下面

c# - 找不到 System.Windows 程序集

wpf - WPF在触发器中设置边框背景

c# - WPF Toolkit DataGrid 如何设置 Column Header SelectionBackground?