Introduction
If you have ever needed transparency in a TextBox
or RichTextBox
for any reason, and have tried to attempt it yourself, you know what a pain it can be. Well... now available for your use is the AlphaTextBox
and AlphaRichTextBox
.
Background
I was online one night looking at my old professor's (Bob Bradley) AlphaBlendTextBox
control. (Please visit Bob's article here and visit his website here.) I really liked the idea, so thought I would attempt it myself. I also saw that the RichTextBox
transparency issue had not been solved in a good way, and really wanted to find that solution. Going off Bob's article on the AlphaBlendTextBox
, I set out on my mission. I used his concepts based on his article, but have not really looked at his code, wanting to find my own solution. I also wanted this to be done purely in .NET with no API calls, not because API calls are bad or anything, just because what I would use the APIs for is already built into .NET. I succeeded. So here it is...
AlphaTextBox
I used the same concept of using a control on top of the TextBox
to create a transparency effect. Instead of a PictureBox
, however, I am using a Panel
. The Panel
class had to be subclassed to support transparency, and pass along any Windows Notification messages to the underlying TextBox
. Because I am not using API calls, I had to supply a delegate function in the AlphaTextBox
that will allow the AlphaPanel
to use the master control's DefWndProc
function. The constructor for the AlphaPanel
looks this way:
protected internal AlphaPanel(AlphaTextBox Master)
{
MasterTb=Master;
this.Size=Master.Size;
this.Location=new Point(0, 0);
ToMasterDel=MasterTb.STClientDel;
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);
}
Utilities
is the external class that contains all Windows message constants, Win32 structures, and "helper methods" for the AlphaControls.
The AlphaTextBox
was fairly simple to implement. The BackColor
property was overridden, and can only be set to Color.Transparent
, unless it is being set internally. A new property, AlphaBackColor
, was created to set the back color of the AlphaTextBox
. I wanted to separate the two as much as possible. When the AlphaTextBox
updates itself, it internally sets base.BackColor
to AlphaBackColor
, issues a WM_PRINT
message through DefWndProc
, and then maps the AlphaBackColor
using a ColorMap
and the new AlphaAmount
property. This bitmap is stored in memory and cloned to the AlphaPanel.BackgroundImage
. This comes in handy when drawing the caret.
To set the caret position, I issued a EM_POSFROMCHAR
message through DefWndProc
. This returns an integer that contains the x coordinate in the low order byte and the y coordinate in the high order byte. Using Bit Shifts and Bit &, I separated the two, then did some string measurements to get the actual position, because the caret is actually one character ahead of where the position is.
To draw the caret, I simply use a graphics object to draw a rectangle at the given position in the foreground color, with a thickness relative to the font size. This is drawn right onto the AlphaPanel.BackgroundImage
eliminating the need for another bitmap. When the caret blinks, the blended bitmap in memory is copied back to AlphaPanel.BackgrondImage
as a clone. In this way, you save memory by only ever having two bitmaps.
All mouse events are passed from the AlphaPanel
to the AlphaTextBox
so that mouse operations work. Many methods and properties had to be overridden for the duplication of the TextBox
workings to be accurate.
In a nutshell, that is the AlphaTextBox
. For more in-depth looks, download the project.
AlphaRichTextBox
Wow!! Who would have thought that a TextBox
and RichTextBox
could be so different? I thought it would be different, but not this different. The RichTextBox
is a completely different animal. A WM_PRINT
message does not work for this guy, unless the UserPaint
flag is set, in which case, all you get is the background color and no text. Hmmm.... That's not going to work. What about BitBlt
, the Win32 graphics method to copy controls to device contexts? Nope... The AlphaPanel
covers the RichTextBox
, so all you get is a good shot of the same thing you had. I kept thinking "Something has got to work for this." Well, eventually, after countless hours of searching the bowels of the MSDN website, success! Something I could use. The RichTextBox
supports a function called FormatRange()
to dump the contents of a RichTextBox
into a device context, usually for printing. Immediately, I recognized the potential of this function. So after some conversions of the Win32 structs for the call.....
[ StructLayout( LayoutKind.Sequential)]
private struct STRUCT_RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
[ StructLayout( LayoutKind.Sequential)]
private struct STRUCT_CHARRANGE
{
public int cpMin;
public int cpMax;
}
[ 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;
}
.....I created a function call to get a portion of the RichTextBox
to a bitmap...
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)
rc.right=ToTwips(MasterControl.ClientSize.Width+
(MasterControl.ClientSize.Width/80F)+4);
else
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();
STRUCT_FORMATRANGE forRange;
forRange.chrg=charRange;
forRange.hdc=hdc;
forRange.hdcTarget=hdc;
forRange.rc=rc;
forRange.rcPage=rcPage;
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);
Marshal.FreeCoTaskMem(lParam);
g.ReleaseHdc(hdc);
}
What is that if
statement for? Well, unfortunately, the Win32 uses twips as its measurement, while .NET uses pixels. There are 14.4 twips in 1 pixel. To my dismay, however, the struct specifying bounding regions, STRUCT_RECT
, only uses integers. This became a huge problem later when the measurements had to be precise. We will come back to this.
So now, I have my control in a bitmap. What now? Well, the background color of the control is not copied, only the text and its attributes, except for highlighting, of course, so when I map the colors to the AlphaRichTextBox
background, I just clear the bitmap to the AlphaBackColor
. Next, I draw the bitmap containing the text onto that bitmap, and wallah... I have an AlphaRichTextBox
with an AlphaBlended background color! But this is only the first step of the process.
Next came the problem of highlighting text when it is selected. Not a concern with the AlphaTextBox
because WM_PRINT
has the control paint itself to the device context, so you get a duplicate of what the control looks like. Not so with the AlphaRichTextBox
. Highlight attributes are not copied with formatRange()
, making necessary a new method for highlighting the text. Drawing over the top of the bitmap is slow and hogs memory, so that was out. I came up with this solution...
I used the EM_SETCHARFORMAT
message to change the attributes of the characters selected in the RichTextBox
. But before that can work, I had to do something to keep track of the attributes of each character before they were changed. For that, I came up with the RichTextInformation
and RichTextInformationCollection
classes. RichTextInformation
contains a Font
, BackColor
, and ForeColor
attribute for one character. RichTextInformationCollection
extends CollectionBase
and contains attributes for a range of characters.
With that in place, I converted the Win32 CHARFORMAT2
structure...
[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;
}
...and used the EM_SETCHARFORMAT
message to highlight the selected text, and stored the old values in a RichTextInformation
object. Now, when more text is selected, or when the current text is deselected, all I had to do was restore the attributes, and bingo, an AlphaRichTextBox
with highlighting capability.
One of the last things I had to do was with the caret. When you have two different font sizes on one line, the baseline shifts down for the larger font. When you get the caret position for the smaller font, it draws the caret above the text. Not good. So for that problem, I used the EM_LINEINDEX
message to get the start index of the line with the caret, then EM_LINELENGTH
to get the length of the line. Then, I went character by character through the line, getting the font sizes, established a baseline, and drew the caret from that baseline.
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;
Color back=Color.Empty, fore=Color.Empty;
TBUtils.GetRTHighlight(ref back, ref fore, AlphaBackColor);
Font selFnt=this.SelectionFont;
while(lineStart<lineEnd)
{
this.Select(lineStart, 1);
if(this.SelectionFont.Height>height)
height=this.SelectionFont.Height;
lineStart++;
}
Now, back to the if
statement from above. As stated earlier, the STRUCT_RECT
structure only takes integers as values, creating a problem when transposing the blended bitmap to the AlphaPanel
. Because you can't use floats, the measurements for margins become inaccurate, and the caret is drawn either out of the control all together, or on the next or previous line from where it should. When the scrollbar is set to Vertical
, and not ForcedVertical
, it changes the measurements around when the scrollbar appears. This became a mess. I had to play around with the numbers for a while before I got something that worked correctly in both situations. The if
statement checks to see weather the vertical scrollbar is present or not, then calculates the twips based on that. I have done a lot of testing with these measurements, and they appear to work all of the time, but they still may be a bit off in some instances.
That is pretty much the problem of the AlphaRichTextBox
solved.
Points of Interest
I really hope this helps someone. It was a bit of a pain in the you know what. There were way more issues in the development of the AlphaRichTextBox
than I listed here. It seemed like every time I got something to work, it caused a problem somewhere. The RichTextBox
definitely is a hard control to inherit. There are still a few issues, but I do intend to resolve them when I get time. Your comments and opinions are welcome!
History