Gruhn's Transparent Delphi App

Download Project

Actually, it doesn't have to be a Delphi app. That's the source I'll provide and the context in which I will discuss it, but the transparency is implemented strictly through WinAPI calls which should be easy to translate to any capable development environment. I will try to keep it as stripped down as possible.

This method can be used to create a window with arbitrary parts "cut out". That is, the "transparent" parts of the window will actually be missing, not there, click through to the stuff underneath. For all intents and purposes, there is no window there. This does not create a window that can be clicked on but just happens to be see through. There is a quick hack approach to that with some of its own drawbacks. Check here.

Start a new Application.

Set BorderStyle to bsNone.

Save and run your app. It probably works, but it's nice to get this out of the way.

With no border, your app will display as a plain gray rectangle. You'll have to hit Alt-F4 to close it.

Add a routine for the form's OnCreate event. Make it like so :

procedure TForm1.FormCreate(Sender: TObject);
    NewRgn : HRGN;
  NewRgn := CreateEllipticRgn(10,10,width-10,height-10);

And then make sure that "Windows" is in your Uses clause.

Run the app. Your window is now a dull gray circle. Again, you'll need to hit Alt-F4 to close it. Told you it would be stripped down and minimal.

So, what's going on here? Windows has a concept of "regions". I don't know anything about regions actually, so I'll guess for now. A region is a portion of a window defined by a border. Windows provides a few functions for dealing with regions. Windows provides the region handle as the primary interface for programmers to handle regions. The most basic functions are those that create regions. In this case, I used CreateEllipticRgn to create an elliptical region defined by its bounding rectangle. I chose a rectangle a little smaller than the window. Just because. The region is, at this stage, an abstract bunch of geometric data not tied to a window or any such.

The key region function is SetWindowRgn. It takes the handle for the window to have its region set, the region to be given to the window and whether or not the window should be redrawn. The coordinates from the region are applied to the window's full extent, using window local (not screen) coordinates.

It really is that easy. You create a region then you set the appliction's window region to that region.

Now it's time to start illuminating some more details. Add a routine for the OnPaint event. Make it like so :

procedure TForm1.FormPaint(Sender: TObject);
  canvas.pen.width := 10;

Run. My app filled in white with a nice black border. I presume that yours did as well unless you have different windows colours or defaults or something.

Back to the Object Inspector and change the main form's BorderStyle to bsSingle.

Run the app. Now the ellipse drawn in OnPaint is shifted down and you can maybe even see a bit of title bar up at the top. Why? When you SetWindowRgn, that region is relative to the window's full extent, including non-client areas. The painting in the canvas is relative to the window's client area. The border style bsSingle includes borders, titlebar and buttons. All sorts of non-client decoration. That's why I had you set the BorderStyle to bsNone back at the beginning. You can, if you would rather have a different border style, use TCustomeForm.ClientRect, TCustomForm.ClientWidth, TCustomForm.ClientHeight, TControl.left and TControl.right to work out the math for putting everything where you want it.

Put BorderStyle back to bsNone, please.

Yeah, what about moving the form? How do you move the form? Well, you could do some wild and annoying stuff, or you could let Windows handle it for you. When the user clicks in a window, Windows asks the window if the user clicked in a non-client area, and if so, what kind of non-client area. We can, when asked, tell Windows that the user clicked on the title bar. Windows then says to itself "Oh, well then, we'd better start moving that window." Which it does.

Add a private method to your form. Declare it like so :

    procedure WMNCHitTest(var Msg:TMessage); message WM_NCHITTEST;

Implement it like so :

procedure TForm1.WMNCHitTest(var Msg:TMessage);
  msg.result := HTCAPTION;

Run the app. Click and drag somewhere in the app. The app moves.

Windows sends the wm_nchittest message to your app when the user clicks. You respond by saying that the user clicked on the caption. Windows takes it for granted that it should then move the application. Easy as that. I can never remember the return values possible for non-client hit test, but I do remember that they are in windows.pas somewhere. Eventually I either find them, or remember that "caption" is something useful to search for. Once you get there, you can see all the other toys.

More Complex Regions. Regions don't have to be simple rectangles or ellipses. They can be ... well frankly, they can be anything. Concave, disjoint, speckled. Check out the list of CreateXRgn functions. (Click on CreateEllipticRgn, hit F1 and click the Group button in the help.) There's rectangles and rounded rectangles and arbitrary polygons. And more importantly, there's the ability to combine regions. OK, here's an idea for a simple simple combine region application. Instead of one, we'll use two ellipses!

Turns out that there is a little bit of extra overhead involved when you start playing around with more regions. Check the help for SetWindowRgn and you'll see that it says down near the bottom that you should not delete the region that you hand to Windows for the window. It needs it. But if we combine two regions, and hand one of them to Windows, that leaves us with an extra region kicking around in memory. That needs to be cleaned up. Here's code :

procedure TForm1.FormCreate(Sender: TObject);
    LeftRgn : HRGN;
    RightRgn : HRGN;
    NewRgn : HRGN;
  LeftRgn  := CreateEllipticRgn(10,10,width div 2, height-10);
  RightRgn := CreateEllipticRgn(width div 2, 10, width-10, height-10);
  NewRgn := CreateRectRgn(0,0,0,0);

And since we have two ellipses, we need to change the code in FormPaint. But cut and paste from the CreateEllipticRgn calls works and is easy.

procedure TForm1.FormPaint(Sender: TObject);
  canvas.pen.width := 10;
  canvas.Ellipse(10,10,width div 2, height-10);
  canvas.Ellipse(width div 2, 10, width-10, height-10);

Run the app. Should have two ellipses. You may have seen goggling eye applications that use this. Now you know.

Take a close look at the space between the ellipses. A single pixel gap. Note that somewhere in the documentation, I think for CreateRectRgn it points out that the region created is exclusive of the right and bottom edges. Well, there we go. Explains that.

But I've a little more explaining to do. What's with the NewRegion := CreateRectRgn()? Quite simply, CombineRgn does not create a new region. The destination region must exist. Hand it a bogus region handle, and it isn't happy and the SetWindowRgn fails. Try it if you wan't. Comment out the CreateRectRgn line and see what you get. I got a big unmodified rectangular window. So I create some arbitrary region then Windows is happy at the CombineRgn. Note that because the region is (0,0,0,0) and the right and bottom are eliminated, this just might create an empty region. Fine with me, I wasn't using it and overwrote it at the CombineRgn. No biggie.

Of course, see that I was diligent and called DeleteObject to get rid of the left and right regions when I was done with them. Alway be remembering to delete any region you create except for the one that is passed to Windows through SetWindowRgn.

Note that in CombineRgn, the combine mode is RGN_OR. That's a bitwise or, not an English conceptual type. The obvious way to mess this up, to me, is to think "Oh, I want the left one AND the right one. So I should use RGN_AND." That would be wrong. It's a bitwise AND. Since the regions don't overlap, there is no part that has both of them so the resulting region would be empty and you'd be doomed.

HELP So, what if you do that? Make a window with no region? It can be very hard to find a window that has no drawing parts. But it will show up on the tool bar and you can right click and close from there. Or, if you run it, notice that you can't see it, it will be the active application and you can just Alt-F4 it to death.

One last thing that I can think of. Too big regions. Sneak into the code and change that right ellipse to be larger than the window :

  RightRgn := CreateEllipticRgn(width div 2, 10, width+60, height+60);

Notice that the ellipse is formed properly and large, but that Windows keeps track of how big the window REALLY is and only lets SetWindowRgn have effect inside the actual bounding rectangle of the window. If you need to have a window that will grow outside of its borders, you have to make the window larger by setting the window's actual size and location.

That's really all there is to it. You can get pretty darned creative with this simple tool if you want.

I have a form somewhere that OnCreate, creates an empty region then iterates through its Controls[], creating a new region for each and combining that into the master region. Then when the form pops up, all of it's dull gray areas are gone but each control has exactly a nice little rectangle reserved for it to perch in.

I have another app that runs through a bitmap and where it finds white pixels, adds little rectangles to a region. In the end, you get a perfect bitmap shaped window. I use mine to show a bunny.

I have another app that rotates the main rectangle in a circle. This is done by using CreatePolygonRgn and using my own code to rotate the points. Next, I have to make controls that rotate too.

You can use window regions on any windowed control.

I have a control that when put on a form will remove it's shape from the parent form. OR add it back in.

Peter M. Gruhn