I was putting together a WinForms utility application today that consists of a TabStrip control on the main dialog box. On a themed system the TabStrip displays a nice gradual gradient on the page. As part of this application I wanted to output statistical and detailed information about the user's selections in a readonly fashion, but I wanted to have the text appear as though it were written directly on the TabStrip page rather than within a control.
Of course my first thought was to use a Label control, but I almost immediately wrote that off as the amount of information to be displayed was prohibitive and variable. The data being displayed might exceed the space on the TabStrip, so scrolling the information is a must.
Next I thought of using a TextBox control, but without some pretty intense subclassing and overriding, I wouldn't be able to achieve the 'written-on' look I was after. The TextBox will always render an opaque background color. Sure, there are ways of faking transparency, like overriding TextBox and adding a PictureBox and telling the parent control to WM_PRINT into it, but that seems to be a kludgy way of getting the results I wanted.
Additionally, there are mechanisms to use a RichTextBox (v5.0) to achieve transparency that are well documented online, but try as I might with these implementations, I found them to be quite buggy. In particular, the appearance of the scrollbars was erratic and sometimes didn't render properly with the control was resized. After battling with it for a few hours I gave up. It wasn't for lack of desire, it just wasn't worth it given the scope of the project I was working on.
So this morning I went back to the drawing board and came up with a solution in about 45 minutes that seems to do exactly what I wanted. I've called this control the ScrollableLabel. Essentially, it offers the readonlyness of a Label control and the scrollability of a TextBox. This implementation here is very fundamental. I plan on enhancing it some more to incorporate user-defined colors, headers, and much more.
I made some executive decisions about how this control should render. For instance, it will always use your system's colors for text and I set the BackColor property of the UserControl to Transparent via the designer. For both of these I plan on making them user-selectable choices, but for not, they're the look I was after.
public partial class ScrollableLabel : UserControl {
public ScrollableLabel() {
InitializeComponent();
SetStyle(ControlStyles.Selectable, false);
// the preferred mechanism for enabling double-buffering is to set the DoubleBuffered property
// of the control rather than the equivalent SetStyle(DoubleBuffer | UserPaint | AllPaintingInWmPaint, true).
DoubleBuffered = true;
AutoScroll = true;
AutoScrollMinSize = Size.Empty;
}
private SizeF _actualTextSize;
public override string Text {
get { return base.Text; }
set {
base.Text = value;
calcLabelDimensions();
Invalidate();
}
}
protected override void OnScroll(ScrollEventArgs se) {
base.OnScroll(se);
Invalidate();
}
public void Clear() {
Text = string.Empty;
}
#region Hidden design-time properties
[Browsable(false)]
public override bool AutoScroll {
get { return true; }
set { /* do nothing for now; control is always AutoScroll */ }
}
[Browsable(false)]
public override Color ForeColor {
get { return SystemColors.ControlText; }
set { /* do nothing for now; only ControlText is currently supported */ }
}
#endregion
private void calcLabelDimensions() {
using ( Graphics g = CreateGraphics() ) {
// assume that no text should wrap
_actualTextSize = g.MeasureString(Text, Font);
AutoScrollMinSize = Size.Round(_actualTextSize);
}
}
protected override void OnPaint(PaintEventArgs e) {
RectangleF rct = new RectangleF(AutoScrollPosition, _actualTextSize);
Brush br = Enabled
? SystemBrushes.ControlText
: SystemBrushes.GrayText;
e.Graphics.DrawString(Text, Font, br, rct);
}
}
When this control is added to a Form it will automatically add scrollbars if the contents exceed the dimensions of the control. At design-time, however I wanted to make sure that I could see where the ScrollableLabel was positioned so I created a simple Designer.
public sealed class BorderlessBorderDesigner : ControlDesigner {
protected override void OnPaintAdornments(PaintEventArgs pe) {
base.OnPaintAdornments(pe);
if ( BorderStyle.None == getBorderStyle() ) {
Rectangle rct = Control.ClientRectangle;
rct.Width -= 1;
rct.Height -= 1;
using ( Pen p = new Pen(SystemColors.ControlDark) ) {
p.DashStyle = DashStyle.Dash;
pe.Graphics.DrawRectangle(p, rct);
}
}
}
private BorderStyle getBorderStyle() {
UserControl ctl = Control as UserControl;
if ( null != ctl ) return ctl.BorderStyle;
return BorderStyle.None;
}
}
Then, I added [Designer(typeof(BorderlessBorderDesigner)] to my ScrollableLabel class declaration.
It's a simple control that achieves a simple goal. I'll post my updates to it as I get a chance to modify it and make it all the more flexible and powerful.
Enjoy!