I needed an OSD-like window to show some important information from my WinForms application. I looked everywhere for samples in C#/.NET, but the samples that I could find were weak (from my point of view) and written in C++ (or C). So, I had no choice but to write my own OSD window (hereafter, just OW). But the excellent article "Floating, collapsible transparent window in C#" by olvio had some great code that helped me a lot. Thank you very much, olvio!
First of all, what is an OSD? OSD is a short text message, displayed to the user when some event occurs – like the user presses the key, file downloading is complete, a new mail has arrived, and so on. An OSD (and OW, obviously) must possess three main characteristics:
- Should always be the top most window: User must see our message even if he/she is working in Word, IE, or reading a mail.
- Should always be completely transparent: User must see the text, and not the background of OW.
- Should always be completely transparent to mouse/keyboard events: This means that the user must be able to select an icon positioned "under" OW through OW. Or, if the user is working in Word, and OSD's text is displayed "above" Word's main area and the user moves the mouse pointer in OSD's client area, then the mouse pointer must not change its appearance to "Arrow". Instead, it must keep the "Text select" form. In other words, the mouse/keyboard doesn't "see" OW, but the user does. :) Needless to say, OW must be transparent to the system as a whole – no button on the task bar, no entry in Task Manager, etc.
The suggested solution completely fulfills all the three requirements. Moreover, it offers additional features (optional, of course) – a bunch of animation effects when OW is shown/hidden, the OSD text itself may be fully opaque, fully transparent, or in between, and some others.
As said earlier, the great code from olvio serves as the starting point. But I have "stripped off" a few redundant functions from the original
FloatingWindow and kept only the required ones for OSD. So, my
FloatingWindow is much, much simpler than the original. To be brief, the
FloatingWindow is created as a
System.Windows.Forms.NativeWindow with some special
WS_EX_TOPMOST helps OW to be the top most window.
WS_EX_TRANSPARENT helps OW to be completely transparent (by sight).
WS_EX_LAYERED (this flag is supported by Windows 2000 and above!) and
WS_EX_TOOLWINDOW help OW to be completely transparent to the mouse/keyboard/system.
Now, we can show a new window (with or without animation) and also hide it (also with optional animation). From this window inherits
FloatingOSDWindow, our main "work horse". It overrides the
PerformPaint() method of the base class to show the OSD text, and gives us the public method
Show(…) with many parameters. As a matter of fact, I have used the OW suffice to know what each parameter of this single method means, and nothing more. Let's jump to this method.
Using the code
We instantiate the new OW by calling a parameter-less constructor:
FloatingOSDWindow osd1 = new FloatingOSDWindow();
Now, we are ready to call
public void Show(Point pt, byte alpha,
Color textColor, Font textFont, int showTimeMSec,
AnimateMode mode, uint time, string text)
The purpose of these parameters:
pt - Coordinate of the top-left corner of OW in screen coordinates.
alpha - Degree of transparency of the OSD text. This is not the transparency of OW's background. This window doesn't have the background. So this parameter is for the text's transparency. As you may expect, 0 means totally transparent (no text) and 255 means totally opaque – any object "under" the text is hidden. As a general rule, any value in the range 100-210 is fine.
textColor - OSD text color.
textFont - OSD text font. Again, the font must be large enough (18-48 point) to attract the user's attention.
showTimeMSec - For how long the OSD text will be displayed on the screen, in milliseconds. After this, OW will be automatically closed and disposed.
mode - One of the
enum members. Determines the effect that will be used when OW opens/closes. Also see the next parameter,
time - If this parameter is zero, no animation effect is used, the
mode parameter is just ignored, and OW opens/closes at once. If it is greater than 0, it specifies how long it takes to play the animation, in milliseconds. This time is not included in
showTimeMSec but used twice – when OW opens, and when it closes. Example:
showTimeMSec = 3500 and
time = 500. In this case, after calling
Show(…), for 0.5 sec, OW will be opened with the effect specified in
mode. Next, the fully opened window will be displayed for 3.5 sec. Finally, during the same 0.5 sec, OW will be closed with the same effect. Again, a general (if we want animation) value in the range 100-500 is fine for this parameter.
text - OSD text itself, at last. :)
Example of calling the function:
osd1.Show(new Point(20, 45), 155, Color.Lime,
new Font("Verdana", 26f, FontStyle.Bold | FontStyle.Italic),
370, "Hello! I am OSD window...");
Show(…), you don't need to call the
Close() method explicitly. See the description of the
showTimeMSec parameter if you have forgotten. But, of course, you can do it if, for example, if you want to force OW to close before
Points of interest
When I tried to override the
PerformPaint() method in the
FloatingOSDWindow class to provide the drawing of the OSD text, my first approach was to draw the string with:
Graphics g = e.Graphics;
It works, but in spite of the AntiAlias rendering mode for the text, the OSD text comes into view with a black edge around the letters. This edging is very thin, but noticeable. So I tried another way – create
GraphicsPath from the given string and fill up this path with the given color:
Graphics g = e.Graphics;
this._gp = new GraphicsPath();
this._gp.AddString("OSD text", …..);
g.SmoothingMode = SmoothingMode.HighQuality;
This approach works perfectly as you can see from the demo project. The interesting problem was with
this._gp.AddString(…). This string of code, as you expect, adds the given text string to the path. One of the parameters for this method,
float emSize, is the font size. Of course, this size must be the same as the size of the
textFont font, which is passed to
Show(…). At first glance, there is no problem: use
textFont.SizeInPoints and we have the needed value. But this is tricky:
float emSize in pixel, but we try to pass it as point. So, we need to convert between points and pixels, which in turn requires detailed knowledge of the DPI of the device we are drawing on. The last value returned by the
Graphics g = e.Graphics;
this._gp.AddString(..., ..., ...,
g.DpiY * textFont.SizeInPoints / 72, ..., ...);
We divide by 72, since point is 1/72 of an inch, no matter on what device we draw. And,
Graphics.DpiY returns in dots (and, in fact, pixels) per inch. So this formula works just fine.
This solution was developed under VS2005/FW2.0. So it may not open in VS2003. But the code will, no doubt, work in VS2003/FW1.1(1.0). Just create a new solution and use the *.cs files from the pack.
- 1/1/2006: Version 1.0 - First release.
- 5/25/2006: Version 1.1 - Fixed problem with handles leak.