.NET (290) administrative (42) Ajax (42) AngularJS (1) ASP.NET (144) bicycle (2) books (178) browser (8) C# (128) cars (1) chess (27) CodePlex (10) Coma (8) database (46) deployment (3) Entity Framework (2) essay (109) flash/shockwave (2) flex (1) food (3) friend (2) game (20) idea (5) IIS (8) javascript (81) LInQ (2) Linux (6) management (4) manga (43) misc (663) mobile (1) movies (88) MsAccess (1) murder (2) music (65) mysql (1) news (98) permanent (1) personal (66) PHP (1) physics (2) picture (308) places (12) politics (13) programming (495) rant (118) religion (3) science (40) Sharepoint (3) software (57) T4 (2) technology (11) Test Driven Development (4) translation (2) VB (2) video (100) Visual Studio (44) web design (45) Windows API (8) Windows Forms (3) Windows Server (4) WPF/Silverlight (63) XML (11)

Monday, November 02, 2009

Things I've learned from HotBabe.NET

Rebecca Romijn as the icon of HotBabe
Today I've released version 1.2 of the HotBabe.NET application. It is a program that stays in the traybar, showing a transparent picture, originally of a woman, sitting on top of your other applications. Clicks go through the image and the opacity of the picture is set so that it doesn't bother the use of the computer. When the CPU use or the memory use or other custom measurements change, the image changes as well. The original would show a girl getting naked as the use of CPU went up. Since I couldn't use what images it should use, I did my own program in .NET. This blog post is about what I have learned about Windows Forms while creating this application.

Step 1: Making the form transparent



Making a Windows Form transparent is not as simple as setting the background transparent. It needs to have:
  • FormBorderStyle = FormBorderStyle.None
  • AllowTransparency = true
  • TransparencyKey = BackColor
However, when changing the Opacity of the form, I noticed that the background color would start showing! The solution for this is to set the BackColor to Color.White, as White is not affected by opacity when set as TransparencyKey, for some reason.

Step 2: Making the form stay on top all other windows



That is relatively easy. Set TopMost = true. You have to set it to true the first time, during load, otherwise you won't be able to set it later on. I don't know why, it just happened.

Update: I noticed that, even when TopMost was set, the image would vanish beneath other windows. I've doubled the property with setting WS_EX_TopMost on the params ExStyle (see step 4).

Step 3: Show the application icon in the traybar and hide the taskbar



Hiding the taskbar is as easy as ShowInTaskbar = false and putting a notification icon in the traybar is simple as well:
_icon = new NotifyIcon(new Container())
{
Visible = true
};
Set the ContextMenu to _icon and you have a tray icon with a menu. There is a catch, though. A NotifyIcon control needs an Icon, an image of a certain format. My solution was, instead of bundling an icon especially for this, to convert the main girl image into an icon, so I used this code.

Step 4: Hide the application from Alt-Tab, make it not get focus and make it so that the mouse clicks through



In order to do that, something must be done at the PInvoke level, in other words, using unsafe system libraries. At first I found out that I need to change a flag value which can be read and written to using GetWindowLong and SetWindowLong from user32.dll. I needed to set the window style with the following attributes:
WS_EX_Layered (Windows Xp/2000+ layered window)
WS_EX_Transparent (Allows the windows to be transparent to the mouse)
WS_EX_ToolWindow (declares the window as a tool window, therefore it does not appear in the Alt-Tab application list)
WS_EX_NoActivate (Windows 2000/XP: A top-level window created with this style does not become the foreground window when the user clicks it).

Then I found out that Form has a virtual method called CreateParams giving me access to the style flag value. Here is the complete code:

protected override CreateParams CreateParams
{
get
{
CreateParams ws = base.CreateParams;

if (ClickThrough)
{
ws.ExStyle |= UnsafeNativeMethods.WS_EX_Layered;
ws.ExStyle |= UnsafeNativeMethods.WS_EX_Transparent;
}
// do not show in Alt-tab
ws.ExStyle |= UnsafeNativeMethods.WS_EX_ToolWindow;
// do not make foreground window
ws.ExStyle |= UnsafeNativeMethods.WS_EX_NoActivate;
return ws;
}
}


However, the problem was that if I changed ClickThrough, it didn't seem to do anything. It was set once and that was it. I noticed that changing Opacity would also set the click through style, so I Reflector-ed the System.Windows.Forms.dll and looked in the source of Opacity. Something called UpdateStyles was used (This method calls the CreateParams method to get the styles to apply) so I used it.

Update: Apparently, the no activate behaviour can also be set by overriding ShowWithoutActivation and returning true. I've set it, too, just to be sure.

Step 5: Now that the form is transparent and has no border or control box, I can't move the window around. I need to make it draggable from anywhere



There is no escape from native methods this time:

private void mainMouseDown(object sender, MouseEventArgs e)
{
// Draggable from anywhere
if (e.Button == MouseButtons.Left)
{
UnsafeNativeMethods.ReleaseCapture();
UnsafeNativeMethods.SendMessage(Handle,
UnsafeNativeMethods.WM_NCLBUTTONDOWN,
UnsafeNativeMethods.HT_CAPTION, 0);
}
}
Both ReleaseCapture and SendMessage are user32.dll functions. What this mouse down event handler does is say to the Window that no matter where it was clicked, it actually clicked the draggable area.

Step 6: Remove flicker



Well, I am getting a bit ahead of myself, here, the flickering becomes annoying only when I implement the blending of an image into another, but since it is also a style setting, I am putting it here:

SetStyle(ControlStyles.AllPaintingInWmPaint
| ControlStyles.UserPaint
| ControlStyles.OptimizedDoubleBuffer, true);
This piece of code, placed in the Form constructor, tells the form to use a double buffer for drawing and to not clear the form before drawing something else.

Update: It seems the same thing can be achieved by setting the Control property DoubleBuffer to true as it seems to be setting ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint and ControlStyles.UserPaint seems to be set by default.

Step 7: Blend the images one into the other



Well, in order to make an image blend nicely into the next, I used a Timer. 10 times a second I would decrease the opacity of the first, increase the opacity of the second and draw them one over the other.

A small detour: if you think about it, this is not absolutely correct. A 70% opacity pixel blocks 70% of the light and lets 30% of the image behind show. If the image underneath has 30% opacity, then it shows 30% left from 30% of the image and it doesn't get opaque. But if I just set the opacity of the bakground image to 100%, it shows really strong on the parts of the images that are not overlapping, where image1 is transparent and image2 is not.

Unfortunately there is no resource friendly way to read write/pixels. It's either GetPixel/SetPixel in a Bitmap class (which are very slow) or using pinvoke again. I prefered to use the opacity hack which looks ok.

I was already using an extension method to invoke any change on the form on its own thread (as the Timer ran on its own thread and I would have gotten the "Cross-thread operation not valid: Control [...] accessed from a thread other than the thread it was created on" exception or "Invoke or BeginInvoke cannot be called on a control until the window handle has been created"):

public static void SafeInvoke(this Control control, Action action)
{
if (control.IsDisposed)
{
return;
}
if (!control.IsHandleCreated)
{
try
{
action();
}
catch (InvalidOperationException ex)
{
}
return;
}
if (control.InvokeRequired)
{
control.BeginInvoke(action);
}
else
{
action();
}
}

This is where I got the "Object is currently in use elsewhere" InvalidOperationException. Apparently the Image class is not thread-safe, so both the timer and the form were trying to access it. I tried locking the setter and getter of the Image property on the class responsible with the image blending effect, with no real effect. Strangely enough, the only solution was to Clone the image when I move it around. I am still looking for a solution that makes sense!

Step 8: Showing the window while dragging it



Using WS_EX_NOACTIVATE was great, but there is a minor inconvenience when trying to move the form around. Not only that the image is not shown while moving the form (On my computer it is set to not show the window contents while dragging it), but the rectangular hint that normally shows is not displayed either. The only way to know where your image ended up was to release the mouse button.

It appears that fixing this is not so easy as it seems. One needs to override WndProc and handle the WM_MOVING message. While handling it, a manual redraw of the form must be initiated via the user32.dll SetWindowPos method.

The nice part is that in this method you can actually specify how you want the form draw. I have chosen SWP_NoActivate, SWP_ShowWindow and SWP_NoSendChanging as flags, where NoActivate is similar with the exstyle flag, ShowWindow shows the entire form (not only the rectangle hint) and NoSendChanging seems to improve the movement smoothness.

Quirky enough, if I start the application without Click through set, then the rectangle hint DOES appear while dragging the window. And with my fix, both the image and the rectangle are shown, but not at the same time. It is a funny effect I don't know how to fix and I thought it was strange enough to not bother me: the rectangle is trying to keep up with the hot babe and never catches on :)

Step 9: Dragging custom images



I am a programmer, that means that it is likely to add too many features in my creations and never make any money out of them. That's why I've decided to add a new feature to HotBabe.NET: droppping your own images on it to display over your applications.

At first I have solved this via ExStyle, where a flag tells Windows the form accepts files dragged over it. A WndProc override handling the WM_DROPFILES message would do the rest. But then I've learned that Windows Forms have their own mechanism for handling file drops.

Here are the steps. First set AllowDrop to true. Then handle the DragEnter and DragDrop events. In my implementation I am checking that only one file is being dropped and that the file itself can be converted into an image BEFORE I display the mouse cursor hint telling the user a drop is allowed. That pretty much makes the ugly MessageBox that I was showing in the previous implementation unnecessary.

Step 10: Reading files from web, FTP, network, zip files, everywhere, with a single API



Reading and writing files is easy when working with local files, using System.IO classes, but when you want to expand to other sources, like web images or files bundles in archives, you get stuck. Luckily there is a general API for reading files using URI syntax in the System.Net.WebRequest class. Here is a sample code for reading any file that can be represented as an URI:

WebRequest req = WebRequest.Create(uriString);
using (var resp = req.GetResponse())
{
using(var stream= resp.GetResponseStream())
{
// do something with stream
}
}


WebRequest can also register your own classes for specific schemas, others than http, ftp, file, etc. I created my own handler for "zip:" URIs and now I can use the same code to read files, web resources of zipped files.

One point that needs clarification is that at first I wanted an URI of the format "zip://archive/file" and I got stuck when the host part of the URI, mainly the archive name, would not accept spaces in it. Instead use this format "schema:///whatever" (three slashes). This is a valid URI in any situation, as the host is empty and does not need to be validated in any way.

The rest are details and you can download the source of HotBabe.NET and see the exact implementation. Please let me know what you think and of any improvement you can imagine.

1 comment:

Andrzej said...

Great article, valuable info about low-level tricks, just fixed redrawing of my WPF app when dragging thanks to your tips. I am updating my simple on-screen keyboard after one year :-) http://wosk.codeplex.com/