使用 WPF FormattedText 类在 Windows Forms 应用程序中绘制格式化文本
如何在 System.Drawing.Graphics 对象上绘制多行格式化文本。
引言
最近,我遇到了一个需求,需要在 .NET DataGridView
的某一列中渲染一些简单的标记文本(例如 Lorem <b>ipsum</b> dolor sit amet, <u><i>consectetur adipisicing</u> elit, sed do eiusmod</i> tempor incididunt)。所以我决定将任务分成两部分:解析文本以获取格式化信息,并编写一个带有自定义 Paint
方法的自定义 DataGridViewCell
。对于后者,我必须将格式化文本渲染到提供的 Graphics
对象中,这就是本文要讲的内容。
背景
将多行格式化文本绘制到 Graphics
对象中并不像听起来那么简单。 .NET 平台中没有开箱即用的功能可以做到这一点。我发现了一些使用 RTF 控件的解决方案,但这开销太大了。接下来,我尝试使用 Font
和 Graphics
类提供的字体度量、字符串测量等手动编码,但很快就决定不走这条路——要做好这项工作可能需要几周时间。在寻找解决方案时,我还发现了 System.Windows.Media.FormattedText
类,这正是我所需要的,但它是 WPF,无法直接在 Graphics
对象上绘制。所以我又回到那里,试图找出如何让它为我工作,这就是我想出的办法。
工作原理
FormattedText
类接受一个字符串,并允许您以非常简单的方式格式化任意字符范围。然后,您可以提供 MaxTextWidth
和 MaxTextHeight
设置来定义多行渲染的布局矩形。如果文本不适合该矩形,FormattedText
对象将根据需要显示省略号。
在本文中,采取以下步骤将输出绘制到 Graphics
对象中
在 WPF 方面
- 根据需要创建和配置
FormattedText
- 在
DrawingVisual
上绘制FormattedText
- 将
DrawingVisual
渲染到RenderTargetBitmap
中
WPF 和 Windows Forms 之间的接口
- 创建一个
System.Drawing.Bitmap
- 将
RenderTargetBitmap
像素复制到位图的像素缓冲区
在 Windows Forms 方面
- 在
Graphics
对象上绘制位图
循序渐进
我们从给定的字体和前景色开始,通常从我们想要绘制格式化文本的窗体或控件中获取。我们还有一个图形和一个布局矩形可以绘制。
Dim font As System.Drawing.Font
Dim color As System.Drawing.Color
Dim rectangle As System.Drawing.Rectangle
Dim graphics As System.Drawing.Graphics
步骤 1:创建和配置 FormattedText
FormattedText
构造函数接受以下参数
Dim textToFormat As String
Dim culture As System.Globalization.CultureInfo
Dim flowDirection As System.Windows.FlowDirection
Dim typeface As System.Windows.Media.Typeface
Dim emSize As Double
Dim foreground As System.Windows.Media.Brush
作为要格式化的文本,我们使用 Lorem ipsum
textToFormat = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " +
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " +
"quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " +
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore " +
"eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, " +
"sunt in culpa qui officia deserunt mollit anim id est laborum."
然后我们必须从我们的 Windows Forms 参数中推导出其他参数。
对于文化,我们只从当前线程获取文化信息
culture = System.Globalization.CultureInfo.CurrentCulture
从文化中获取流向
If culture.TextInfo.IsRightToLeft Then
flowDirection = System.Windows.FlowDirection.RightToLeft
Else
flowDirection = System.Windows.FlowDirection.LeftToRight
End If
根据字体创建字体类型
typeface = New System.Windows.Media.Typeface(font.FontFamily.Name)
对于 emSize
,我们必须进行转换:FormattedText
期望 emSize
以设备无关单位表示,即 1/96 英寸。字体为我们提供了以磅为单位的大小,每磅是 1/72 英寸。所以
emSize = font.SizeInPoints * 96.0 / 72.0
使用我们颜色的 ARGB 字节可以轻松创建前景色画笔
foreground = New System.Windows.Media.SolidColorBrush( _
System.Windows.Media.Color.FromArgb( _
color.A, color.R, color.G, color.B))
现在我们有足够的信息来创建一个 FormattedText
对象
Dim formattedText = New System.Windows.Media.FormattedText( _
textToFormat, culture, flowDirection, typeface, emSize, foreground)
现在我们可以将字体对象的样式信息应用于我们的格式化文本:与 FontStyle
属性粗体、斜体、下划线和删除线对应的 WPF 等效项。
Dim fontStyle As System.Windows.FontStyle
Dim fontWeight As System.Windows.FontWeight
Dim textDecorations = New System.Windows.TextDecorationCollection
If (font.Style And System.Drawing.FontStyle.Italic) <> 0 Then
fontStyle = System.Windows.FontStyles.Italic
Else
fontStyle = System.Windows.FontStyles.Normal
End If
If (font.Style And System.Drawing.FontStyle.Bold) <> 0 Then
fontWeight = System.Windows.FontWeights.Bold
Else
fontWeight = System.Windows.FontWeights.Normal
End If
If (font.Style And System.Drawing.FontStyle.Underline) <> 0 Then
textDecorations.Add(System.Windows.TextDecorations.Underline)
End If
If (font.Style And System.Drawing.FontStyle.Strikeout) <> 0 Then
textDecorations.Add(System.Windows.TextDecorations.Strikethrough)
End If
formattedText.SetFontStyle(fontStyle)
formattedText.SetFontWeight(fontWeight)
formattedText.SetTextDecorations(textDecorations)
现在我们有了一个与我们初始字体和颜色对应的格式化文本。让我们对文本应用一些范围格式:从字符索引 5 开始的 50 个字符斜体,从字符索引 20 开始的 30 个字符特粗,以及从字符索引 0 开始的 20 个字符上划线。
formattedText.SetFontStyle(System.Windows.FontStyles.Italic, 5, 50)
formattedText.SetFontWeight(System.Windows.FontWeights.ExtraBold, 20, 30)
formattedText.SetTextDecorations(System.Windows.TextDecorations.OverLine, 0, 20)
步骤 2:在 Drawing Visual 上绘制 FormattedText
在绘制格式化文本之前,我们必须使用其 MaxTextWidth
和 MaxTextHeight
属性将布局矩形应用于格式化文本。这些属性期望以设备无关单位表示的值,但我们的矩形以像素为单位。因此,我们必须使用当前 Graphics
对象的 DPI 设置将像素转换为设备无关单位(1/96 英寸)
formattedText.MaxTextWidth = rectangle.Width / (graphics.DpiX / 96.0)
formattedText.MaxTextHeight = rectangle.Height / (graphics.DpiY / 96.0)
有关 DPI、磅、设备无关单位等的更多详细信息,请参见此处:http://msdn.microsoft.com/en-us/library/windows/desktop/ff684173%28v=vs.85%29.aspx。
设置 MaxTextWidth
和 MaxTextHeight
属性后,FormattedText
对象现在将自行布局在给定矩形内,执行自动换行并根据需要应用省略号。所以现在我们可以在 WPF DrawingVisual
上绘制格式化文本。
Dim drawingVisual = New System.Windows.Media.DrawingVisual
Using drawingContext = drawingVisual.RenderOpen()
drawingContext.DrawText(formattedText, New System.Windows.Point(0, 0))
End Using
步骤 3:将 DrawingVisual 渲染到 RenderTargetBitmap 中
此时,我们必须获取格式化文本的度量值——Width
和 Height
——并将它们转换为像素以创建 RenderTargetBitmap
。
首先,我们必须使用宽度和高度属性测量格式化文本。同样,这些属性以设备无关单位表示,必须转换为像素。然后我们可以创建 RenderTargetBitmap
。
Dim pixelWidth = System.Convert.ToInt32( _
System.Math.Ceiling(formattedText.Width * (graphics.DpiX / 96.0)))
Dim pixelHeight = System.Convert.ToInt32( _
System.Math.Ceiling(formattedText.Height * (graphics.DpiY / 96.0)))
Dim rtb = New System.Windows.Media.Imaging.RenderTargetBitmap( _
pixelWidth, pixelHeight, graphics.DpiX, graphics.DpiY, _
System.Windows.Media.PixelFormats.Pbgra32)
步骤 4:创建一个 System.Drawing.Bitmap
与 Pbgra32
对应的 Windows Forms PixelFormat
是 Format32bppPArgb
;位图的宽度和高度与源 RenderTargetBitmap
相同
Dim bitmap = New System.Drawing.Bitmap(rtb.PixelWidth, rtb.PixelHeight, _
System.Drawing.Imaging.PixelFormat.Format32bppPArgb)
步骤 5:将 RenderTargetBitmap 像素复制到位图的像素缓冲区
我们使用 LockBits
函数访问 Bitmap
对象的像素缓冲区,然后我们可以使用 RenderTargetBitmap.CopyPixels
方法
Dim pdata = bitmap.LockBits(New System.Drawing.Rectangle( _
0, 0, bitmap.Width, bitmap.Height), _
System.Drawing.Imaging.ImageLockMode.WriteOnly, bitmap.PixelFormat)
rtb.CopyPixels(System.Windows.Int32Rect.Empty, _
pdata.Scan0, pdata.Stride * pdata.Height, pdata.Stride)
bitmap.UnlockBits(pdata)
步骤 6:在 Graphics 对象上绘制位图
我们快完成了,最后一步是在 Graphics
对象上绘制 Bitmap
对象,位于布局矩形的起点:
graphics.DrawImage(bitmap, rectangle.Location)
性能
这样做的目的是将格式化文本直接绘制在窗体和控件上,因此性能对于 Windows Forms 应用程序 GUI 的速度和响应能力至关重要。
我使用 SharpDevelop IDE 中的分析器,结果如下:
更昂贵的操作是
- 在
DrawingVisual
上绘制文本 - 将
DrawingVisual
渲染到RenderTargetBitmap
中 Bitmap
构造函数- 在
Graphics
对象上绘制Bitmap
- 绘制前测量
FormattedText
(Width
和Height
属性)
不那么昂贵的操作是
- 将像素从
RenderTargetBitmap
复制到Bitmap
- 设置文本格式参数
- 绘制后测量
FormattedText
(Width
和Height
属性)
文本测量中的性能差异存在,因为 FormattedText
使用缓存的度量。这些度量在文本绘制或测量时被缓存,并在文本/范围属性更改时失效。如果您访问测量属性且没有有效的缓存度量,FormattedText
对象将在内部绘制文本以获取度量。
考虑到这些结果,我编写了一个名为 WindowsFormsFormattedText
的可重用代码类,试图通过仔细缓存 DrawingVisual
、RenderTargetBitmap
和 Bitmap
来最大化性能。
例如,为了避免 Bitmap
构造函数,它只使用一个在类级别共享的 Bitmap
——只有当缓存的 Bitmap
小于 RenderTargetBitmap
时才创建新的 Bitmap
。如果它相等或更大,则重用。这需要在使用缓存位图的代码上进行同步,但这比一直创建 Bitmap
或在对象级别缓存 Bitmap
要便宜得多。
为了使缓存高效工作,我还需要知道 FormattedText
对象中是否存在缓存度量。由于该信息是 FormattedText
私有的,因此使用反射来获取它。
有关实现细节,您可以参考本文附带的 WindowsFormsFormattedText
类。
使用相同的分析器对 Graphics.DrawString
和 WindowsFormsFormattedText.Draw
进行性能比较表明,在最坏情况下(无法缓存 DrawingVisual
和 RenderTargetBitmap
),Graphics.DrawString
比 WindowsFormsFormattedText.Draw
快两到四倍,但 WindowsFormsFormattedText
仍然足够快,可以在具有出色 GUI 性能的 Windows Forms 应用程序中使用。
使用代码
附加的 Zip 包含两个类
WindowsFormsFormattedText
完成所有工作,并且FormattedTextLabel
是一个派生自Label
的控件,可以在窗体等上使用。
C# 用户注意事项:代码应该很容易转换,只需将其通过 Developer Fusion 的代码转换工具即可,该工具可在网上和 SharpDevelop IDE 中免费获得。
将类添加到您的项目并引用两个程序集:FoundationCore 和 WindowsBase。这些程序集自 3.0 版本起已包含在框架中,因此无需外部依赖项。
WindowsFormsFormattedText
类内部维护一个 FormattedText
对象,并公开其所有与文本格式化相关的成员。这些成员在此处有文档:http://msdn.microsoft.com/en-us/library/system.windows.media.formattedtext.aspx
此外,它还添加了带有 Windows Forms 领域参数的方法和属性,以自动化所需的转换。例如,除了 TextWidth
属性外,还有一个 TextPixelWidth(dpiX)
属性。对于内置的 SetTypeface
方法,有等效的 SetFont
方法等等。
SetStyle(System.Drawing.FontStyle)
方法一次性设置所有 Windows Forms 字体样式属性(粗体、斜体、下划线和删除线)。因此,如果您执行例如 SetStyle(FontStyle.Bold Or FontStyle.Italic, 20, 30)
,它将为给定字符范围删除下划线和删除线,这可能不是您所期望的。要设置单个样式属性而不影响其他样式属性,您必须使用内置方法 SetFontWeight
、SetFontStyle
和 SetTextDecorations
。
注意:
方法在第一个上传版本中是一个
SetStyle(System.Drawing.FontStyle)
SetFontStyle
重载,但我将其重命名,因为它具有误导性:在 FormattedText
中,该方法仅适用于普通、斜体或倾斜。
它还公开了一个 Draw(Graphics, ...)
方法,该方法自动化了在 Graphics
对象上绘制格式化文本。
FormattedTextLabel
公开了一个 FormattedText
属性,提供对内部 WindowsFormsFormattedText
对象的访问,允许格式化要在标签上显示的文本。为了方便起见,它派生自 Label
,更好的实现可能直接派生自 Control
。但标签不是我的目标,我写它只是为了测试。
闪烁:由于在 Graphics
对象上使用 Bitmap
进行绘制,在某些情况下可能会发生闪烁。如果您遇到闪烁,请考虑双缓冲闪烁的控件。
这里有一些示例代码。要使用它,请创建一个窗体并在其上放置两个按钮,一个名为 PerformanceTestButton
,另一个名为 LabelTestButton
。
- 用于性能测试的代码
- 用于测试
FormattedTextLabel
的代码
Private endPerformanceTest As Boolean
Private performanceTestForm As Form
Private Sub PerformanceTestButton_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles PerformanceTestButton.Click
performanceTestForm = New Form
performanceTestForm.Text = "Performance test - close form to finish."
performanceTestForm.CreateControl()
performanceTestForm.Font = New Font(performanceTestForm.Font.FontFamily, 12.0!)
Dim t = New System.Threading.Thread(AddressOf PerformanceTest)
t.Start()
performanceTestForm.ShowDialog()
endPerformanceTest = True
End Sub
Sub PerformanceTest()
Dim rnd = New System.Random
Dim textSample = "Lorem ipsum dolor sit amet, consectetur adipisicing elit," & _
" sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " & _
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris " & _
"nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in " & _
"reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " & _
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " & _
"officia deserunt mollit anim id est laborum."
Dim fText = New WindowsFormsFormattedText(textSample, performanceTestForm.Font)
endPerformanceTest = False
Do
Dim x = rnd.Next() Mod 10
Dim y = rnd.Next() Mod 10
Dim w = 10 + (rnd.Next() Mod (performanceTestForm.Width - 20))
Dim h = 10 + (rnd.Next() Mod (performanceTestForm.Height - 20))
Using g = performanceTestForm.CreateGraphics()
fText.MaxTextPixelWidth(g.DpiX) = w
fText.MaxTextPixelHeight(g.DpiY) = h
Try
fText.Draw(g, x, y)
Catch ex As Exception
System.Diagnostics.Debug.Print(ex.Message)
End Try
Try
g.DrawString(textSample, performanceTestForm.Font, Brushes.Black, _
New RectangleF(x, y, w, h), System.Drawing.StringFormat.GenericDefault)
Catch ex As Exception
System.Diagnostics.Debug.Print(ex.Message)
End Try
End Using
Loop Until endPerformanceTest
End Sub
Private Sub LabelTestButton_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles LabelTestButton.Click
Dim f = New Form()
f.Text = "FormattedTextLabel Test"
f.Width = 600
f.Height = 400
Dim l = New FormattedTextLabel
l.Text = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " & _
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " & _
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi " & _
"ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit " & _
"in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " & _
"Excepteur sint occaecat cupidatat non proident, sunt in culpa " & _
"qui officia deserunt mollit anim id est laborum."
l.FormattedText.SetFontStyle(FontStyle.Bold Or FontStyle.Italic, 0, 30)
l.FormattedText.SetFontSizeInPoints(18.0, 200, 60)
l.FormattedText.SetForegroundColor(Color.Aquamarine, 100, 120)
l.BackColor = Color.White
l.BorderStyle = BorderStyle.FixedSingle
l.SetBounds(50, 50, 500, 300)
l.Anchor = AnchorStyles.Bottom Or AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top
f.Controls.Add(l)
f.ShowDialog()
End Sub
这是结果。
我希望此代码对您的项目有用。在我的下一篇文章中,我将介绍基于 WindowsFormsFormattedText
和一个我称之为 SimpleMarkupText
的辅助类(它将带有 <b>、<u>、<font color...> 等简单标记的文本解析到 WindowsFormsFormattedText
对象中)开发 DataGridViewFormattedTextCell
和 DataGridViewFormattedTextColumn
。
历史
- 文章提交:2012 年 8 月。
- 更新文本和下载:8 月 22 日
- 将
重命名为
SetStyle(System.Drawing.FontStyle)
SetStyle(System.Drawing.FontStyle) - 添加了
Draw(graphics, ..., clipBounds)
重载:考虑到通常在 OnPaint 重写或 Paint 事件中提供的 clipBounds (或 clipRectangle) 矩形,当绘图表面被其他窗口部分覆盖或超出屏幕时,性能可以得到改善。