.NET Forum / Windows Forms / WinForm Controls / July 2007
How to change colour of TextBox border
|
|
Thread rating:  |
Notre Poubelle - 16 Jul 2007 18:08 GMT Hello,
I'm trying to figure out how to change the border colour of the TextBox control. I can't find a property that does this. I've researched this but couldn't find a complete sample of anyone achieving this.
For the Textbox control, the overriden OnPaint method is not called unless the control takes full responsibility for drawing the Textbox control but that's not something I want to do; I just want to change the border colour. A suggestion I found was to override the WM_PAINT message in WndProc. I did this, and could use the Graphics.FromHwnd method to get access to GDI. I could then draw a rectangle, but it was only in the interior of the TextBox, not the outside so it was clipping the user entered text.
I think I need to draw to the non-client area (is that right?) of the TextBox control. I handled the WM_NCPAINT message in WndProc and then borrowed & adapted a routine I found on the web that looks like this (after the WndProc):
protected override void WndProc(ref Message m) { if (m.Msg == 0x85) //WM_NCPAINT { PaintNonClientArea(m.HWnd, (IntPtr)m.WParam); return; } base.WndProc(ref m); }
private void PaintNonClientArea(IntPtr hWnd, IntPtr hRgn) { RECT windowRect = new RECT(); if (NativeMethods.GetWindowRect(hWnd, out windowRect) == false) return;
Rectangle bounds = new Rectangle(0, 0, windowRect.Right - windowRect.Left, windowRect.Bottom - windowRect.Top);
if (bounds.Width == 0 || bounds.Height == 0) return;
Region clipRegion = null; if (hRgn != (IntPtr)1) clipRegion = System.Drawing.Region.FromHrgn(hRgn);
IntPtr hDC = NativeMethods.GetDCEx(hWnd, hRgn, (DeviceContextValues.Window | DeviceContextValues.IntersectRgn | DeviceContextValues.Cache | DeviceContextValues.ClipChildren));
if (hDC == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
using (Graphics g = Graphics.FromHdc(hDC)) { using (Pen pen = new Pen(Brushes.Green)) { Rectangle myRect2 = new Rectangle(); myRect2.X = 0; myRect2.Y = 0; myRect2.Width = bounds.Width - 1; myRect2.Height = bounds.Height - 1; g.DrawRectangle(pen, myRect2); } }
}
The problem is that the call to GetDCEx always returns 0, and the Win32 exception thrown says invalid 'The parameter is incorrect'. The second argument hdC seems to be what's causing the problem. This usually evaluates to 1 (from the WM_NCPAINT message) but when it gets passed into GetDCEx, I get the failure. Any idea what I'm doing wrong?
Thanks, Notre
Mick Doherty - 16 Jul 2007 23:21 GMT Don't use the IntersectRegion flag.
You may be interested in this thread: http://groups.google.co.uk/group/microsoft.public.dotnet.framework.windowsforms. controls/browse_thread/thread/c201083a1ca664b1/f39894ed2a2f39e3?lnk=st&q=&rnum=1 #f39894ed2a2f39e3
 Signature Mick Doherty http://www.dotnetrix.co.uk/nothing.html
> Hello, > [quoted text clipped - 81 lines] > Thanks, > Notre Notre Poubelle - 17 Jul 2007 00:10 GMT Hi Mick,
Thank you very much for your reply. I did try your suggestion of removing the IntersectRegion flag, but I'm still having the problem that the return value of GetDCEx is 0. I glanced at the second post you mentioned, and I noticed that you used several different flags for painting to the non-client area. I also noticed that the second argument you passed into GetDCEx is IntPtr.Zero. I was passing in the value of hRgn, which I got from the WM_NCPAINT message. As noted in the original post, this seems to always evaluate to 1 for me, and this is when the GetDCEx call is failing. Was there a particular reason why you pass in IntPtr.Zero rather than what is provided via the WM_NCPAINT message?
I will take some time to read through the other post you referenced. It looks to have lots of valuable information that I've so far come nowhere close to thinking about, never mind solving :)
Thanks! Notre
Mick Doherty - 17 Jul 2007 10:40 GMT Hi Notre,
If m.wParam always returns 1 then you are probably painting a LayeredWindow. Is this Vista?
WM_NCPAINT passes either 1 or a Region handle to m.WParam. When it's 1 the entire window needs repainting.
The Second parameter of GetDCEx is a Region Handle. 1 is not a region handle and so the call fails. Since LayeredWindows always require the entire window to be painted, the DCX_*REGION flags also cause the method to fail.
Have fun and get the Aspirin ready ;-)
 Signature Mick Doherty http://www.dotnetrix.co.uk/nothing.html
Notre Poubelle - 17 Jul 2007 18:32 GMT Hi Mick,
No, this isn't Vista. It's Windows 2003, using XP themes.
Thank you for the explanation re: GetDCEx & the region handle. That makes some sense. I don't know how to tell whether I'm using a LayeredWindow, but I don't think I'm explicitly doing anything to ask for one.
Thanks, Notre
Notre Poubelle - 17 Jul 2007 21:16 GMT Hi Mick,
I tried your example of the NCControlBase & that worked quite well. I then tried to apply the same code to my specialization of the TextBox control but it didn't behave nearly as well. The top of the border does indeed seem to be painting outside the textbox client area, so that's great. But the rectangle size is not right and I'm seeing clipping of text as well as some strange painting artifacts.
In the NCPaint method, what is the significance of setting the X and Y coordinates of the 'r' rectangle to 4? Why 4? The number 4 also appears to be a magic number in the WM_NCCALCSIZE message handler. Could you please explain its use there as well?
Thanks, Notre
Mick Doherty - 18 Jul 2007 00:42 GMT Hi Notre,
The example class I posted Inherits from Control, which has no Non-Client Area. To Add a Non-Client Area I have intercepted the WM_NCCALCSIZE method and changed the return value so that it includes a 4 pixel border. You could set it to any value you like.
TextBox already has a Non-Client Area and so you don't need to Intercept the WM_NCCALCSIZE method unless you want to change it's size, but I wouldn't recommend this.
If you change Inheritance of the example class from Control to TextBox, then you will end up with a 5 or 6 pixel border as the textbox has a 1 or 2 pixel border already. Modifying the WM_NCCALCSIZE method as I have done, results in the Client Area being made smaller rather than the Non-Client Area being expanded outwards. This is why the Text does not fit any more. The strange artifacts that you are seeing are due to the ClipRegion passed to m.wParam in the WM_NCPAINT message. We need to modify it so that Windows does not paint over our Non-Client Painting.
TextBox is a particularly nasty control to modify in this way. The Win32 Edit class, which it wraps, has some Scrollbar bugs, and the border paints differently depending upon whether it's FixedSingle or Fixed3D. With Fixed3D we don't need to worry about clipping out the scrollbars, as the border paints outside of them, but with FixedSingle we do. I'm still not 100% on getting this right but, judging by the ScrollBar bugs in TextBox, neither are the MS Windows devs ;-)
If you wish to use Fixed Single then the Clipping becomes quite complex, but if you're happy to just keep the border at Fixed3D then the following class should work fine.
\\\ using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Runtime.InteropServices; using System.Windows.Forms;
namespace Dotnetrix.Samples.CSharp { class TextBoxEx : TextBox { private Color borderColor = Color.Black;
public Color BorderColor { get { return borderColor; } set { if (borderColor != value) { borderColor = value; NativeMethods.SendMessage(this.Handle, NativeMethods.WM_NCPAINT, (IntPtr)1, IntPtr.Zero); } } }
protected override void OnResize(EventArgs e) { base.OnResize(e); NativeMethods.SendMessage(this.Handle, NativeMethods.WM_NCPAINT, (IntPtr)1, IntPtr.Zero); }
protected override void WndProc(ref Message m) { if (m.Msg == NativeMethods.WM_NCPAINT && this.BorderStyle == BorderStyle.Fixed3D) { if (this.Parent != null) { NCPaint(); m.WParam = GetHRegion(); base.DefWndProc(ref m); NativeMethods.DeleteObject(m.WParam); m.Result = (IntPtr)1; } } base.WndProc(ref m); }
private void NCPaint() { if (this.Parent == null) return; if (this.Width <= 0 || this.Height <= 0) return; if (this.BorderStyle != BorderStyle.Fixed3D) return;
IntPtr windowDC = NativeMethods.GetDCEx(this.Handle, IntPtr.Zero, NativeMethods.DCX_CACHE | NativeMethods.DCX_WINDOW | NativeMethods.DCX_CLIPSIBLINGS | NativeMethods.DCX_LOCKWINDOWUPDATE);
if (windowDC.Equals(IntPtr.Zero)) return;
using (Graphics g = Graphics.FromHdc(windowDC)) {
Rectangle borderRect = new Rectangle(0, 0, Width, Height);
using (Pen borderPen = new Pen(this.borderColor, 2)) { borderPen.Alignment = PenAlignment.Inset; g.DrawRectangle(borderPen, borderRect); } }
////Clean Up NativeMethods.ReleaseDC(this.Handle, windowDC);
}
private IntPtr GetHRegion() { //Define a Clip Region to pass back to WM_NCPAINTs wParam. //Must be in Screen Coordinates. IntPtr hRgn; Rectangle winRect = this.Parent.RectangleToScreen(this.Bounds); Rectangle clientRect = this.RectangleToScreen(this.ClientRectangle);
Region updateRegion = new Region(winRect); updateRegion.Complement(clientRect);
using (Graphics g = this.CreateGraphics()) hRgn = updateRegion.GetHrgn(g); updateRegion.Dispose(); return hRgn; }
}
internal class NativeMethods {
[DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
[DllImport("gdi32.dll")] public static extern bool DeleteObject(IntPtr hObject);
[DllImport("user32.dll")] public static extern IntPtr GetDCEx(IntPtr hWnd, IntPtr hrgnClip, int flags);
[DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
public const int WM_NCPAINT = 0x85;
public const int DCX_WINDOW = 0x1; public const int DCX_CACHE = 0x2; public const int DCX_CLIPCHILDREN = 0x8; public const int DCX_CLIPSIBLINGS = 0x10; public const int DCX_LOCKWINDOWUPDATE = 0x400;
} } ///
If you want to see the Scrollbar bugs just drop a standard TextBox on a Form and set the following properties:
MultiLine = True WordWrap = False ScrollBars = both RightToLeft = Yes
 Signature Mick Doherty http://www.dotnetrix.co.uk/nothing.html
Mick Doherty - 18 Jul 2007 11:48 GMT oops, Forgot to allow for LayeredWindow (you will have a LayeredWindow if you change Forms Opacity or TransparencyKey).
\\\ using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Runtime.InteropServices; using System.Windows.Forms;
namespace Dotnetrix.Samples.CSharp { class TextBoxEx : TextBox { private Color borderColor = Color.Black;
public Color BorderColor { get { return borderColor; } set { if (borderColor != value) { borderColor = value; NativeMethods.SendMessage(this.Handle, NativeMethods.WM_NCPAINT, (IntPtr)1, IntPtr.Zero); } } }
protected override void OnResize(EventArgs e) { base.OnResize(e); NativeMethods.SendMessage(this.Handle, NativeMethods.WM_NCPAINT, (IntPtr)1, IntPtr.Zero); }
protected override void WndProc(ref Message m) { if (m.Msg == NativeMethods.WM_NCPAINT && this.BorderStyle == BorderStyle.Fixed3D) { if (this.Parent != null) { NCPaint(); m.WParam = GetHRegion(); base.DefWndProc(ref m); NativeMethods.DeleteObject(m.WParam); m.Result = (IntPtr)1; } } base.WndProc(ref m); }
private void NCPaint() { if (this.Parent == null) return; if (this.Width <= 0 || this.Height <= 0) return; if (this.BorderStyle != BorderStyle.Fixed3D) return;
IntPtr windowDC = NativeMethods.GetDCEx(this.Handle, IntPtr.Zero, NativeMethods.DCX_CACHE | NativeMethods.DCX_WINDOW | NativeMethods.DCX_CLIPSIBLINGS | NativeMethods.DCX_LOCKWINDOWUPDATE);
if (windowDC.Equals(IntPtr.Zero)) return;
using (Bitmap bm = new Bitmap(this.Width, this.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb)) {
using (Graphics g = Graphics.FromImage(bm)) {
Rectangle borderRect = new Rectangle(0, 0, Width, Height);
using (Pen borderPen = new Pen(this.borderColor, 2)) { borderPen.Alignment = PenAlignment.Inset; g.DrawRectangle(borderPen, borderRect); }
//Create and Apply a Clip Region to the WindowDC using (Region Rgn = new Region(new Rectangle(0,0,Width,Height))) { Rgn.Exclude(new Rectangle(2, 2, Width-4,Height-4)); IntPtr hRgn = Rgn.GetHrgn(g); if (!hRgn.Equals(IntPtr.Zero)) NativeMethods.SelectClipRgn(windowDC, hRgn);
IntPtr bmDC = g.GetHdc(); IntPtr hBmp = bm.GetHbitmap(); IntPtr oldDC = NativeMethods.SelectObject(bmDC, hBmp); NativeMethods.BitBlt(windowDC, 0, 0, bm.Width, bm.Height, bmDC, 0, 0, NativeMethods.SRCCOPY);
NativeMethods.SelectClipRgn(windowDC, IntPtr.Zero); NativeMethods.DeleteObject(hRgn);
g.ReleaseHdc(bmDC); NativeMethods.SelectObject(oldDC, hBmp); NativeMethods.DeleteObject(hBmp); bm.Dispose(); } } }
NativeMethods.ReleaseDC(this.Handle, windowDC);
}
private IntPtr GetHRegion() { //Define a Clip Region to pass back to WM_NCPAINTs wParam. //Must be in Screen Coordinates. IntPtr hRgn; Rectangle winRect = this.Parent.RectangleToScreen(this.Bounds); Rectangle clientRect = this.RectangleToScreen(this.ClientRectangle);
Region updateRegion = new Region(winRect); updateRegion.Complement(clientRect);
using (Graphics g = this.CreateGraphics()) hRgn = updateRegion.GetHrgn(g); updateRegion.Dispose(); return hRgn; }
}
internal class NativeMethods {
[DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
[DllImport("gdi32.dll")] public static extern int SelectClipRgn(IntPtr hdc, IntPtr hrgn);
[DllImport("gdi32.dll")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll")] public static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
[DllImport("gdi32.dll")] public static extern bool DeleteObject(IntPtr hObject);
[DllImport("user32.dll")] public static extern IntPtr GetDCEx(IntPtr hWnd, IntPtr hrgnClip, int flags);
[DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
public const int WM_NCPAINT = 0x85;
public const int DCX_WINDOW = 0x1; public const int DCX_CACHE = 0x2; public const int DCX_CLIPCHILDREN = 0x8; public const int DCX_CLIPSIBLINGS = 0x10; public const int DCX_LOCKWINDOWUPDATE = 0x400;
public const int SRCCOPY = 0xCC0020;
} } ///
 Signature Mick Doherty http://www.dotnetrix.co.uk/nothing.html
Notre Poubelle - 18 Jul 2007 23:18 GMT Wow, thanks Mick! I have to admit that I don't understand everything you've done yet (especially the modifications to support the LayeredWindow). I will have to study your sample further. The sample seems to work beautifully. I think I understand most of your explanation (if not yet the code itself) but I'm confused by this statement:
Modifying the WM_NCCALCSIZE method as I have done, results in the Client Area being made smaller rather than the Non-Client Area being expanded outwards
This certainly seems to be true based on my observations, but why does modifying the WM_NCCALCSIZE method as you had done cause the Client area to become smaller in the case of the TextBox?
Thanks, Notre
Mick Doherty - 19 Jul 2007 11:48 GMT Hi Notre,
> Wow, thanks Mick! You're welcome.
> I have to admit that I don't understand everything you've > done yet (especially the modifications to support the LayeredWindow). I > will > have to study your sample further. The sample seems to work beautifully. All I've done to support LayeredWindow is to draw the border using GDI via Interop (BitBlt). For some reason, if you draw directly to the LayeredWindow via GDI+, the drawing gets clipped to a rectangle the size of the ClientArea. This is why the right and bottom edge are not drawn in the first example.
>I think I understand most of your explanation (if not yet the code itself) >but [quoted text clipped - 3 lines] > being > expanded outwards When changing the bordersize, the window cannot change the overall bounds of the control, otherwise it would not conform to your specified size. Since it cannot change the Window Bounds, the only way to increase the size of the border is to shrink the ClientRectangle.
> This certainly seems to be true based on my observations, but why does > modifying the WM_NCCALCSIZE method as you had done cause the Client area > to > become smaller in the case of the TextBox? It is not specicfic to TextBox. It is noticeable in TextBox because TextBox is written so that it has a fixed Window Size, dependant on Font, rather than a Fixed Client Size. There are probably ways to overcome this, but I haven't looked that far into it.
 Signature Mick Doherty http://www.dotnetrix.co.uk/nothing.html
Notre Poubelle - 20 Jul 2007 04:48 GMT Thanks once again for the explanation, Mick. I really appreciate the time you put into answering my questions (both the original plus the follow up questions). I will take this away, study it, and come back if I further questions.
Notre
richie5um@googlemail.com - 18 Jul 2007 10:07 GMT I've achieved this in the past using an "ExtenderProvider". This is really simple and quite powerful.
To help you out I've uploaded some code and a demo to here: http://www.richie5um.plus.com/Downloads/TESTTextBoxProvider.zip
Let me know if this helps.
RichS
Notre Poubelle - 19 Jul 2007 00:44 GMT Thanks RichS, this is quite interesting. I need to spend a bit more time to look at it closer. It looks like, on the surface, it could easily be adapted to work with other controls, other than just TextBox; does that sound right? (I.e. it doesn't seem to really rely on any TextBox specific properties)
Thanks, Notre
Notre Poubelle - 20 Jul 2007 05:08 GMT I played with your sample a little more. It was pretty cool, but I noticed a few problems. When form was maximized, the textbox didn't paint correctly. When I changed the FormBorderStyle to Sizeable, and then resized the form, the TextBox likewise had some painting problems.
I'm pretty happy with what Mick has put together on the other posts, so I'll probably use that. Having said that, your example was very useful as a reference for an extender provider and gave some nice utility functions, so I am quite grateful for that.
Thanks, Notre
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|