About four months ago, when I was working on a project, I wanted to add some buttons on the title bar of windows, near the standard Minimize/Maximize/Close buttons. To do that, I had to draw on the title bar, that is, non-client area of the window. Although my work was useless on that project and I just wasted my time, at least I am now writing this article about what I have done. :-)
In this article, I'll show you how to create special
Graphics objects in C# that you can use to draw anywhere on your window, and in my next article, I will explain how to add buttons on the title bar.
Well, as you now, in Microsoft .NET you need a
Graphics object whenever you want to draw something. The
Graphics object must belong to the object you want to draw on. Usually, you have two options to obtain a
Graphics for a control (Form or whatever derived from
System.Windows.Forms.Control class). First, you can handle the
Paint message of the control, and second, you can call the
CreateGraphics method of the object. In the former case, your drawing will be always on the object! This is because the
Paint event is always raised by the .NET subsystem (wrapped on win32
WM_PAINT message) whenever any portion of your window needs to be drawn (i.e. is invalidated - in terms of C++ win32 programming). In the latter case, you draw on your window whenever you want, not whenever your OS wants. But the drawback is that your painting is there, until something passes over your window!
I think I am going too far from the subject. For more information about painting mechanism on .NET and Windows, refer to MSDN Library (
CreateGraphics method, and Painting and Drawing subsection of Windows GDI in Windows SDK Documentation).
Whichever of two methods you use to obtain the
graphics object, you can only draw on the working area of the window (called client area), that is, anywhere except the border and title bar of your window.
In this article, I am designing a class called
WindowGraphics, that creates a
Graphics object for your entire window, that contains client and non-client area of the window; and by using that, you can draw wherever you want on your window. You can also use it to draw on portions of some controls that you had no control over before. For example, any place on a
TextBox (with usual
Graphics objects, you cannot draw on borders of a
The class also handles a problem on normal
Graphics objects when the
RightToLeftLayout properties of your form are both
true. Details of this problem are explained later on.
Using the Class
After downloading the source files attached to the article, you can easily start using the
WindowGraphics class by including the WindowGraphics.cs file into your project. It is very simple. You start by creating an instance of the
WindowGraphics class, passing to the constructor the control you want to draw on.
WindowGraphics wg = new WindowGraphics( this );
Then, simply use the
Graphics property of the newly created object to do your drawing:
wg.Graphics.DrawLine( Pens.Blue, 0, 0, 100, 100 );
Graphics g = wg.Graphics;
g.DrawString( "I am on the title bar!",
new Font( "Tahoma", 10, FontStyle.Bold ), Brushes.Black, 0, 4 );
g.FillEllipse( Brushes.Black, this.Width - 40, this.Height - 40, 80, 80 );
Finally call the
Dispose method of the object to free any resources it is using. Because this class uses unmanaged resources, it is highly recommended that you do not forget to call the
You can also use the
using block of C# that calls the
Dispose method automatically for you. It is the recommended way:
using ( WindowGraphics wg = new WindowGraphics( this ) )
Graphics g = wg.Graphics;
Also note that, when you work with this
Graphics, the origin is upper left corner of the whole form rectangle, not its client area.
The sample project included in the source code is created using Visual Studio 2008, but the project file should also open in Visual Studio 2005 with no problem, because it is configured to use .NET Framework 2.0.
If you are not interested in details, you can leave the article here, and use the class in your project. But if you are, continue reading.
How Does It Work?
Creating this kind of
Graphics object is not a trivial task in .NET. You have to use native methods and win32 calls.
As some of you know, the easiest way to do this is to call the
GetWindowDC function, passing the handle of your window. The
GetWindowDC function belongs to windows
user32 library. You must import this function first:
[DllImport( "user32" )]
private static extern IntPtr GetWindowDC( IntPtr hwnd );
[DllImport( "user32" )]
private static extern IntPtr ReleaseDC( IntPtr hwnd, IntPtr hdc );
Then, call the method to create a DC* for the entire window, and then create a
Graphics object from the DC:
* DC: Device Context - the objects that in the GDI world are used to draw things. Somehow equivalent to
Graphics in the GDI+.
IntPtr hdc = GetWindowDC( this.Handle );
Graphics g = Graphics.FromHdc( hdc );
ReleaseDC( this.Handle, hdc );
I had used this way in my project at first. Now the problem...
The Problem of RightToLeftLayout
If you will never use right to left forms, you can skip this section, but if you make software for a right to left language (as in my case), or are interested in the subject, read this section.
Although this problem is not related to this kind of
Graphics, I am explaining it because I encountered it here.
You can see that when you set the
RightToLeft properties both to
true, the whole coordinate system at the top level gets mirrored. The origin is no longer upper left corner of the client area of the form; it is upper right corner instead. I said at the top level, because it occurs only in the form itself, not for the child controls. For example, when you put a
Panel in your form, inside the
Panel the origin is still upper left corner of the panel.
Well, you may guess that when this happens, any
Graphics objects you construct for the form must be mirrored. Yes, that's true, but not completely! Try this:
- Create a new Windows Forms Application in Visual Studio.
- Select the From and set the
RightToLeftLayout properties to
- Handle the
Paint event, by double clicking the
Paint event in the Events tab of the Properties panel.
- Draw a simple line:
private void Form1_Paint( object sender, PaintEventArgs e )
e.Graphics.DrawLine( Pens.Blue, 0,0, 100, 100 );
By running the project, you see that the line is started from upper right corner of the form, as expected.
Now move another window over the Form. What do you see? The line will be drawn from upper left corner, as if the
Graphics are not right to left.
Now when the form is behind another window on your desktop, click its icon on the taskbar to bring it to front (or minimize the form, and restore it). The line again is drawn from upper right corner!
I have done a lot of investigations, and tried whatever you thought of to find the reason, and I didn't find anything! It may be a bug in the .NET Framework or Windows. I am using the latest version of .NET Framework and Windows XP at the time of writing this article (Service Pack 3 of XP, and .NET 3.5 SP1 which includes .NET 2 Service Pack 2), and the problem is still there. It happens even in Windows Vista. So I decided to do something else. Here is my solution.
Again, How Does It Work?
I tried to create the
Graphics from something that never is right to left – the whole Desktop.
First, I get the DC for the entire screen, and then do the required transformations and clippings to fit the DC on the visible region of the window. Here is the step-by-step explanation:
IntPtr hdc = GetDC( IntPtr.Zero );
By passing zero to the
GetDC function as the window handle, we get a DC for the entire screen.
IntPtr hrgn = GetVisibleRgn( hWnd );
SelectClipRgn( hdc, hrgn );
We must retrieve the clipping region of the form, and clip the DC with that. Without doing this, we may draw on other windows - places that we do not own.
GetVisibleRgn method is a
private method of our class. It returns a handle to the region that the window is currently clipped to. I'll explain the method later. The
SelectClipRgn is a Win32 API function. It clips the given DC to the given region.
Now the origin of the DC is upper left corner of the screen, but we want it to be upper left corner of our form. So we must move the origin:
Rect rect = new Rect();
GetWindowRect( hWnd, rect );
SetWindowOrgEx( hdc, -rect.left, -rect.top, IntPtr.Zero );
Finally, create your
Graphics from the DC:
Graphics graphics = Graphics.FromHdc( hdc );
You're done. Now the
private IntPtr GetVisibleRgn( IntPtr hWnd )
IntPtr hrgn, hdc;
hrgn = CreateRectRgn( 0, 0, 0, 0 );
hdc = GetWindowDC( hWnd );
int res = GetRandomRgn( hdc, hrgn, 4 );
ReleaseDC( hWnd, hdc );
We create an empty region, get the DC of the window, pass the DC to a special function to retrieve the visible region associated with the window, and release the DC. That special function is
GetRandomRgn. I don't know what the philosophy behind the name of the function is, but I think at first it supposed to do a lot more that just retrieving the clipping region of the window. By the way, it works for us.
The final thing is wrapping them all up in a reusable class. You can see the final class in the code attached with the article.
Why I Wrote This Article, and What You Can Learn From It?
The first reason for me was that I wanted to write something! And I found this class simple enough and well designed. I hope that you learn some tips about Object Oriented design, in addition to a few things about Windows GDI.
This is my first article on CodeProject. It has been a long time since I wanted to write about my projects (almost a few years!). I think I have a lot of good experiences to share with others. This is my first step, and I hope that it will continue well enough. I don't know when I can I write my next article, but I think it is about buttons on the title bar.
If you liked the article, please vote for it, and leave some comments. So I can understand the good points, and correct my mistakes.
- 2008/09/18 - Initial issue
- 2008/09/19 - Fixed a very small bug! Source code updated:
- Line 79 of the WindowGraphics.cs,
hWnd changed to
This is because when retrieving the DC, we pass
GetWindowDC to get the DC for the entire screen, and when releasing the DC we should also pass
- 2008/09/21 - In
GetWindowDC is replaced with
GetDC, to work with Windows Vista