我有一个 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>
结果
最佳答案
那么,接受挑战;-) 该解决方案基于以下想法:
- 将 TextBlock 调整到边框内,并确保呈现整个文本,即使不可见。
- 将文本渲染为位图。
- 检测位图中的字形(即字符)以获得像素精确位置。
- 更新 UI 布局,使文本在边框内居中。
- 如果可能,允许简单、通用的用法。
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
仍处于初始(上/左)位置。不过,有一个简单的解决方案:将所需的偏移量添加到TextBlock
的Margin
中。
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 和字体大小 50:
对于边框大小 50x50 和字体大小 50:
注意:存在 1 像素错误,即文本左侧的空间比右侧的空间厚或薄 1 像素。与顶部/底部间距相同。如果边框的宽度为偶数,而渲染的文本的宽度为奇数,则会发生这种情况(抱歉,未提供子像素完美性)
结论
所提出的解决方案对于任何字体、字体大小和文本都应达到 1 像素的误差,并且易于使用。
如果您还没有注意到,我们对与 CenteredText
的 Elem
属性一起使用的 FrameworkElement
做出了非常有限的假设> 控制。因此,这也应该适用于任何具有透明背景并需要(近乎)完美居中的元素。
关于wpf - 如何将渲染文本精确居中在边框内,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28879606/