In this article, you will learn how to create special Graphics objects in C# that you can use to draw anywhere on your window.
Preface
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, the 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.
Background
Well, as you already know, 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 the MSDN Library (Pain
event, CreateGraphics
method, and Painting and Drawing subsection of Windows GDI in Windows SDK Documentation).
Whichever of the 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 TextBox
).
The class also handles a problem on normal Graphics
objects when the RightToLeft
and 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 Dispose
method!
....
wg.Dispose();
....
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 the 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:
using System;
using System.Runtime.InteropServices;
[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 );
g.Dispose();
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 RightToLeftLayout
and RightToLeft
properties both to true
, the whole coordinate system at the top level gets mirrored. The origin is no longer the 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
RightToLeft
and RightToLeftLayout
properties to true
. - 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 the 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 the 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 the 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.
The 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 GetVisibleRgn
method:
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 );
return hrgn;
}
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 than 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 to 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 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.
History
- 18th September, 2008 - Initial issue
- 19th September, 2008 - Fixed a very small bug! Source code updated:
- Line 79 of the WindowGraphics.cs,
hWnd
changed to IntPtr.Zero
.
This is because when retrieving the DC, we pass IntPtr.Zero
to GetWindowDC
to get the DC for the entire screen, and when releasing the DC we should also pass IntPtr.Zero
.
- 21st September, 2008 - In
CreateGraphics
, GetWindowDC
is replaced with GetDC
, to work with Windows Vista