Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
HomeAnnouncementsFree MagazinesWhite PapersSubmit Content
Discussion GroupsASP.NETWindows FormsLanguages.NET FrameworkVisual Studio.NET
Articles.NET FrameworkASP.NETToolsWindows Forms
.NET DirectoryOpen Source ProjectsUser GroupsWeb Resources
Related Topics
Visual Basic 6SQL ServerMS AccessOther DB ProductsMS Server ProductsMore Topics ...

.NET Forum / Windows Forms / WinForm Controls / July 2007

Tip: Looking for answers? Try searching our database.

How to change colour of TextBox border

Thread view: 
Enable EMail Alerts  Start New Thread
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 Magazines

Get 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 ...

Oracle MagazineNetwork ComputingComputer WorldBio-IT WorldeWeekInformation WeekInfosecurity
 
Sign In
Join
My Latest Posts
My Monitored Threads
My Blog
My Photo Gallery
My Profile
My Homepage

Start New Thread
Enable EMail Alerts
Rate this Thread



©2008 Advenet LLC   Privacy Policy - Terms of Use
This website includes both content owned or controlled by Advenet as well as content owned or controlled by third parties.