Alpha 混合(透明可用) TextBox 和 RichTextBox






4.63/5 (22投票s)
2006年2月24日
8分钟阅读

259634

15198
本文展示了如何制作一个透明/半透明的 TextBox 和 RichTextBox。
引言
如果您曾因为任何原因需要在 TextBox
或 RichTextBox
中实现透明度,并且尝试自己动手,您就会知道这有多么麻烦。现在,您可以直接使用 AlphaTextBox
和 AlphaRichTextBox
了。
背景
有一天晚上,我在网上看到了我老教授(Bob Bradley)的 AlphaBlendTextBox
控件。(请访问 Bob 的文章 此处,并访问他的网站 此处。)我非常喜欢这个想法,所以想自己尝试一下。我还注意到 RichTextBox
的透明度问题还没有得到很好的解决,我一直想找到那个解决方案。借鉴 Bob 关于 AlphaBlendTextBox
的文章,我开始了我的使命。我使用了他文章中的概念,但我并没有真正看过他的代码,我想找到我自己的解决方案。我还想完全在 .NET 中实现,而不使用 API 调用,不是因为 API 调用不好,而是因为我本来要使用 API 的功能 .NET 中已经内置了。我成功了。所以,这就是它...
AlphaTextBox
我采用了在 TextBox
上覆盖一个控件来创建透明效果的相同概念。但我使用的是 Panel
而不是 PictureBox
。Panel
类必须被继承以支持透明度,并将任何 Windows 通知消息传递到底层的 TextBox
。因为我没有使用 API 调用,所以我在 AlphaTextBox
中提供了一个委托函数,允许 AlphaPanel
使用主控件的 DefWndProc
函数。AlphaPanel
的构造函数是这样的:
protected internal AlphaPanel(AlphaTextBox Master)
{
MasterTb=Master;
this.Size=Master.Size;
this.Location=new Point(0, 0);
ToMasterDel=MasterTb.STClientDel;
//panels by default can have transparent backcolors
//so you dont need to set that control style
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.DoubleBuffer,true);
this.SetStyle(ControlStyles.Selectable, false);
PUtils=new Utilities(ToMasterDel, MasterTb);
}//AlphaPanel Constructor For AlphaTextBox
Utilities
是外部类,其中包含 AlphaControls 的所有 Windows 消息常量、Win32 结构和“辅助方法”。
AlphaTextBox
的实现相当简单。BackColor
属性被重写,只能设置为 Color.Transparent
,除非它是在内部设置的。创建了一个新的属性 AlphaBackColor
来设置 AlphaTextBox
的背景色。我想将两者尽可能地分开。当 AlphaTextBox
更新自身时,它会在内部将 base.BackColor
设置为 AlphaBackColor
,通过 DefWndProc
发出 WM_PRINT
消息,然后使用 ColorMap
和新的 AlphaAmount
属性映射 AlphaBackColor
。此位图存储在内存中,并克隆到 AlphaPanel.BackgroundImage
。这在绘制插入符号时非常有用。
为了设置插入符号的位置,我通过 DefWndProc
发出了一个 EM_POSFROMCHAR
消息。这返回一个整数,其中低位字节包含 x 坐标,高位字节包含 y 坐标。使用位移和位与运算,我将两者分开,然后进行一些字符串测量以获得实际位置,因为插入符号实际上比其位置超前一个字符。
要绘制插入符号,我只是使用一个图形对象在给定位置以前景颜色绘制一个矩形,其厚度与字体大小成比例。这直接绘制在 AlphaPanel.BackgroundImage
上,无需额外的位图。当插入符号闪烁时,内存中的混合位图会作为克隆复制回 AlphaPanel.BackgrondImage
。这样,您只需维护两个位图,即可节省内存。
所有鼠标事件都从 AlphaPanel
传递到 AlphaTextBox
,以确保鼠标操作正常工作。为了准确复制 TextBox
的功能,许多方法和属性都必须被重写。
总而言之,这就是 AlphaTextBox
。如果您想深入了解,请下载项目。
AlphaRichTextBox
哇!谁能想到 TextBox
和 RichTextBox
竟然如此不同?我本以为会有所不同,但没想到会差这么多。RichTextBox
完全是另一个级别的存在。WM_PRINT
消息对它不起作用,除非设置了 UserPaint
标志,在这种情况下,您只会得到背景色而没有文本。嗯...这不行。那 BitBlt
呢?Win32 复制控件到设备上下文的图形方法?不行... AlphaPanel
覆盖了 RichTextBox
,所以您只会得到与之前相同的画面。我一直在想“总有什么办法能奏效”。最终,经过在 MSDN 网站深处无数小时的搜索,成功了!我找到了可以用的东西。RichTextBox
支持一个名为 FormatRange()
的函数,用于将 RichTextBox
的内容转储到设备上下文,通常用于打印。我立刻意识到了这个函数的潜力。所以在转换了 Win32 结构以进行调用后...
[ StructLayout( LayoutKind.Sequential)]
private struct STRUCT_RECT
{
public int left;
public int top;
public int right;
public int bottom;
}//STRUCT_RECT
[ StructLayout( LayoutKind.Sequential)]
private struct STRUCT_CHARRANGE
{
public int cpMin;
public int cpMax;
}//STRUCT_CHARRANGE
[ StructLayout( LayoutKind.Sequential)]
private struct STRUCT_FORMATRANGE
{
public IntPtr hdc;
public IntPtr hdcTarget;
public STRUCT_RECT rc;
public STRUCT_RECT rcPage;
public STRUCT_CHARRANGE chrg;
}//STRUCT FORMATRANGE
...我创建了一个函数调用来获取 RichTextBox
的一部分到位图...
protected internal void FormatRange(Graphics g,
int startChar, int endChar)
{
STRUCT_CHARRANGE charRange;
charRange.cpMin=startChar;
charRange.cpMax=endChar;
STRUCT_RECT rc;
rc.top=0;
rc.bottom=ToTwips(MasterControl.ClientSize.Height+40);
rc.left=0;
if(MasterControl.Size.Width-MasterControl.ClientSize.Width==20)
//VScrollbar present
rc.right=ToTwips(MasterControl.ClientSize.Width+
(MasterControl.ClientSize.Width/80F)+4);
else
//VScrollbar not present
rc.right=ToTwips(MasterControl.ClientSize.Width +
(MasterControl.ClientSize.Width/100F)+5);
STRUCT_RECT rcPage;
rcPage.top=0;
rcPage.bottom=ToTwips(MasterControl.Size.Height);
rcPage.left=0;
rcPage.right=ToTwips(MasterControl.Size.Width);
IntPtr hdc = g.GetHdc();
//This is what specifies all the information
//for drawing to the bitmap
STRUCT_FORMATRANGE forRange;
forRange.chrg=charRange;
forRange.hdc=hdc;
forRange.hdcTarget=hdc;
forRange.rc=rc;
forRange.rcPage=rcPage;
//We have to send and IntPtr as the lParam. You cant simply
//make a pointer to forRange, so allocate memory and Marshal
//it to an IntPtr
IntPtr lParam=Marshal.AllocCoTaskMem(Marshal.SizeOf(forRange));
Marshal.StructureToPtr(forRange, lParam, false);
SendMessageToMaster(EM_FORMATRANGE, (IntPtr)1, lParam, 1);
SendMessageToMaster(EM_FORMATRANGE,
IntPtr.Zero, IntPtr.Zero, -1);
//release resources
Marshal.FreeCoTaskMem(lParam);
g.ReleaseHdc(hdc);
}//FormatRange
那个 if
语句是做什么用的?嗯,不幸的是,Win32 使用 twips 作为度量单位,而 .NET 使用像素。每 1 像素有 14.4 twips。然而,让我沮丧的是,指定边界区域的结构 STRUCT_RECT
只使用整数。这后来成为了一个**巨大的**问题,因为需要精确的度量。
所以现在,我的控件在一个位图中。接下来呢?嗯,控件的背景色没有被复制,只有文本及其属性(当然,高亮除外),所以当我将颜色映射到 AlphaRichTextBox
背景时,我只是将位图清除为 AlphaBackColor
。然后,我将包含文本的位图绘制到那个位图上, voilà... 我得到了一个具有 Alpha 混合背景色的 AlphaRichTextBox
!但这只是过程的第一步。
接下来是如何处理文本选中的高亮问题。对于 AlphaTextBox
来说这不是问题,因为 WM_PRINT
会让控件自行绘制到设备上下文,您会得到控件外观的复制。但对于 AlphaRichTextBox
就不是这样了。formatRange()
不会复制高亮属性,因此需要一个新方法来高亮文本。在位图上绘制(Drawing over the top of the bitmap)速度慢且占用内存,所以这个方法被排除了。我提出了以下解决方案:
我使用了 EM_SETCHARFORMAT
消息来更改 RichTextBox
中选定字符的属性。但在那之前,我必须做一些事情来跟踪每个字符在被更改之前的属性。为此,我设计了 RichTextInformation
和 RichTextInformationCollection
类。RichTextInformation
包含一个字符的 Font
、BackColor
和 ForeColor
属性。RichTextInformationCollection
继承自 CollectionBase
,并包含一个字符范围的属性。
有了这些,我转换了 Win32 的 CHARFORMAT2
结构...
[StructLayout(LayoutKind.Sequential)]
private struct CHARFORMAT2
{
public int cbSize;
public int dwMask;
public int dwEffects;
public int yHeight;
public int yOffset;
public int crTextColor;
public byte bCharSet;
public byte bPitchAndFamily;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]
public string szFaceName;
public UInt16 wWeight;
public UInt16 sSpacing;
public int crBackColor;
public int lcid;
public int dwReserved;
public UInt16 sStyle;
public UInt16 wKerning;
public byte bUnderlineType;
public byte bAnimation;
public byte bRevAuthor;
public byte bReserved1;
}//struct charformat2
...并使用了 EM_SETCHARFORMAT
消息来高亮选中的文本,并将旧值存储在 RichTextInformation
对象中。现在,当选中更多文本,或者取消当前文本选中时,我只需要恢复属性,搞定,一个具有高亮功能的 AlphaRichTextBox
就完成了。
我需要做的最后几件事之一是关于插入符号。当一行中有两种不同的字体大小时,较大字体的基线会向下偏移。当您获取较小字体的插入符号位置时,插入符号会绘制在文本上方。这不好。所以对于这个问题,我使用了 EM_LINEINDEX
消息来获取插入符号所在行的起始索引,然后使用 EM_LINELENGTH
来获取行的长度。然后,我逐个字符地遍历该行,获取字体大小,建立一个基线,然后从该基线绘制插入符号。
//get the first char in the current line,
// and the length of the current line
int lineStart = (int)(IntPtr)TBUtils.SendMessageToMaster(
TBUtils.EM_LINEINDEX, (IntPtr)(-1), IntPtr.Zero, 1);
int lineEnd = lineStart + (int)(IntPtr)TBUtils.SendMessageToMaster(
TBUtils.EM_LINELENGTH, (IntPtr)lineStart, IntPtr.Zero, 1);
int height=0;
//mark the selection properties before selecting new
//text or you wont ever be able to change anything.
Color back=Color.Empty, fore=Color.Empty;
TBUtils.GetRTHighlight(ref back, ref fore, AlphaBackColor);
Font selFnt=this.SelectionFont;
//get the tallest charachter in the line so you know
//how tall to draw the caret.
while(lineStart<lineEnd)
{
this.Select(lineStart, 1);
if(this.SelectionFont.Height>height)
height=this.SelectionFont.Height;
lineStart++;
}//while
现在,回到上面的 if
语句。如前所述,STRUCT_RECT
结构只接受整数作为值,当将混合位图转换到 AlphaPanel
时会产生问题。由于不能使用浮点数,边距的测量会不准确,插入符号会绘制在控件外部,或者绘制在应该出现的位置的前一行或下一行。当滚动条设置为 Vertical
而不是 ForcedVertical
时,滚动条出现时会改变测量值。这变得一团糟。我不得不花一些时间来调整数字,才在两种情况下都得到了一些正确工作的测量值。那个 if
语句检查垂直滚动条是否存在,然后据此计算 twips。我已经对这些测量值进行了大量测试,它们似乎总是有效,但在某些情况下可能仍然会有些偏差。
基本上,AlphaRichTextBox
的问题就这样解决了。
关注点
我真心希望这对大家有所帮助。这真是太费劲了。AlphaRichTextBox
的开发中遇到的问题比我在这里列出的**多得多**。似乎我每解决一个问题,就会在别的地方引起新的问题。RichTextBox
确实是一个难以继承的控件。仍然存在一些问题,但如果有时间,我打算解决它们。欢迎大家提出评论和意见!
历史
- 版本 1.0
AlphaRichTextBox
的一些问题- 向下滚动时,插入符号有时会出现在最后一个可见行的上方。
- 图像可见,但选中时没有边界框。
- 插入图像后,插入符号并不总是能跳过图像,直到输入更多文本。
这些问题正在解决中。