Tutorial description | |
---|---|
Objective | Create a fancy-looking application that displays the preview of the open applications. |
Covered topics |
|
Requirements |
|
Target audience | Intermediate users |
Download | Source code |
The basic idea behind this application is the following: upon start, we create snapshot (a list) of all the available windows and we use it to decide what previews to display. Once the list is created, a preview is drawn for each item. There is a drawback for this approach: if new windows are created or some existing are closed the interface will not display them (actually for closed windows will replace the preview with an icon). The advantage: is a simple implementation.
There are three parts for this project.
- Enumerating only the open applications’ windows
- Make a glass window
- Generate a live preview for each window
Enumerating windows
Enumerating all windows can be done with the EnumWindows function from user32.dll which can be easily imported in C#.
[DllImport("user32.dll")]
private static extern int EnumWindows(EnumWindowsCallbackDelegate callback, int lParam = 0);
private delegate bool EnumWindowsCallbackDelegate(IntPtr hWnd, int lParam);
However, the result of a pure enumeration will return hundreds of windows. The most powerful filtration is to keep just the windows that are visible. Again, a function from user32.dll called IsWindowVisible is used to check whether a hWnd belongs to a visible window. Probably, after this step you will have just 30-40 windows left in the list.
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hWnd);
The next step is to decide which is the most meaningful representative window from each cluster of windows related by ownership. The Old New Thing blog presents an algorithm for this problem. The logic behind this algorithm is: “For each visible window, walk up its owner chain until you find the root owner. Then walk back down the visible last active popup chain until you find a visible window. If you’re back to where you’re started, then put the window in the Alt+Tab list.” A few Dll imports and the translation of the pseudocode to C# gives the following code.
private static bool IsWindowChainVisible(IntPtr hWnd)
{
// Start at the root owner
IntPtr hwndWalk = GetAncestor(hWnd);
// Basically we try get from the parent back to that window
IntPtr hwndTry;
while ((hwndTry = GetLastActivePopup(hwndWalk)) != hwndTry)
{
if (IsWindowVisible(hwndTry)) break;
hwndWalk = hwndTry;
}
return (hwndWalk == hWnd);
}
At this point you probably filtered most windows. However, there are still a few that refuse to be filtered out:
- The desktop window
- Our application window
- The taskbar
Removing the desktop window
Depending on what you want to do, you may decide to keep this window. The task switcher in Windows 7 provides also the preview for the desktop. However, if you decide to to remove it, you have the check each handle against the desktop handle (which can be obtained using the GetShellWindow from user32.dll).
[DllImport("user32.dll")]
private static extern IntPtr GetShellWindow();
Removing our application window
One naive approach would be to ignore the windows with a specific title. This is bad because many windows can have the same title. The correct approach is to filter based on the window handle. I created a list of ignored handled and, after the main window was created, I added its handle in this list. Because WPF is used, the handle of a window can be obtained using WindowInteropHelper class.
Removing the taskbar
This was tricky and I’m still not sure is the correct approach. There is one window in the list that is not filtered by the previous filters. Even more, is not visible with Spy++ (!!!) and it’s preview is the taskbar. The only solution I found and seems to remove just that window, is to filter out all windows that don’t have the WS_EX_APPWINDOW style. If you find any window that is not displayed because of this, let me know.
Finally, the method that decides which window goes into the snapshot is presented below:
//This function will be called for each available window
private bool EnumWindowsCallback(IntPtr hWnd, int lParam)
{
bool IsDesktopWindow = (hWnd == GetShellWindow());
bool IsVisible = IsWindowVisible(hWnd);
bool IsChainVisible = IsWindowChainVisible(hWnd);
bool IsInIgnoreList = (WindowHandlesToIgnore.Contains(hWnd));
//Filters the taskbar
bool IsApplicationWindow = ((GetWindowLong(hWnd) & WS_EX_APPWINDOW) == WS_EX_APPWINDOW);
if (!IsDesktopWindow && IsVisible && IsChainVisible && !IsInIgnoreList && IsApplicationWindow)
{
windowsSnapshot.Add(new WindowInfo(hWnd));
}
return true;
}
Once we have a snapshot of the visible windows we plan to display them. As seen in the objective screenshot, the main window is transparent and has a glass effect.
The glass window
It is very important to understand that the glass effect is available only on Windows Vista and 7 and works as long as you have the Aero theme enabled!
The glass effect functions are part of the Desktop Window Manager API represented by the library dwmapi.dll. All the functions were imported from that DLL.
In order to make (a part of) a window transparent you need to:
- Make the windows background transparent (paint it with a transparent brush)
- Call the DwmExtendFrameIntoClientArea function specifying the window handle and the inner region of the window that will have the glass effect (you specify the margins from the inner border). If you want the whole window to be transparent specify a negative (-1) margin for all sides.
private void MakeGlassEffect()
{
if (DwmIsCompositionEnabled())
{
IntPtr mainWindowPtr = new WindowInteropHelper(this).Handle;
HwndSource mainWindowSrc = HwndSource.FromHwnd(mainWindowHandle);
mainWindowSrc.CompositionTarget.BackgroundColor = Colors.Transparent;
this.Background = Brushes.Transparent;
MARGINS margins = new MARGINS();
margins.ExtendToWholeClientArea(); //Sets all values to -1
int result = DwmExtendFrameIntoClientArea(mainWindowSrc.Handle, ref margins);
if (result < 0)
{
MessageBox.Show("An error occured while extending the glass unit.");
Application.Current.Shutdown();
}
}
}
Live windows preview
As you might already know, when you press Alt+Tab in Windows Vista/7 (and Aero is active!) you see a live preview of the running applications. In the past, the only method of generating a preview for a window was to copy whatever was visible from it to a bitmap. This approach works as long as the windows is completely visible (no other window is on top, the windows is not out of the screen and the window is not minimized). Trying to save a screenshot with this method will fail, unless you take every window, restore it’s state, bring it to front, take a screenshot and move it back - by the way, this action takes too much time and is messy.
In dwmapi.dll we have a set of functions that will generate thumbnails for any opened window. The interesting part is that the preview is not static! It will modify as the window changes. The function DwmRegisterThumbnail defines a region on which the Desktop Window Manager is allowed to draw the preview for a specific window. So, instead of you drawing the picture, you just specify a region and the DWM will do it for you. One important aspect is that DWM will draw the preview as the top layer - everything on the window will be behind the preview. There are some parameters that you can specify for how the drawing is made (transparency, visibility, etc.) but these are out of the scope of this tutorial.
To ’translate’ the drawing into code we are first going to check if the window has already registered a preview (if so, unregister it), the register a new preview, set the region of the drawing and center the image and finally draw (update) the preview - which will continue to be updated until is unregistered.
private void DrawThumbnail(WindowInfo win, int thumbnailIndex)
{
IntPtr thumbnail = win.Thumbnail;
if (thumbnail != IntPtr.Zero)
DwmUnregisterThumbnail(thumbnail);
int hResult = DwmRegisterThumbnail(mainWindowHandle, win.HWnd, out thumbnail);
if (hResult == 0)
{
PSIZE size;
DwmQueryThumbnailSourceSize(thumbnail, out size);
DWM_THUMBNAIL_PROPERTIES props = new DWM_THUMBNAIL_PROPERTIES();
props.dwFlags = DWM_TNP_VISIBLE | DWM_TNP_RECTDESTINATION | DWM_TNP_SOURCECLIENTAREAONLY;
props.fVisible = true;
props.fSourceClientAreaOnly = true;
//Set the region where the live preview will be drawn
int left = (thumbnailIndex % MaxThumbnails) * (ThumbnailSize + ThumbnailSpacing);
int top = (int)(thumbnailIndex / MaxThumbnails) * (ThumbnailSize + ThumbnailSpacing) + WindowTopOffset;
int right = left + ThumbnailSize;
int bottom = top + ThumbnailSize;
props.rcDestination = new RECT(left, top, right, bottom);
//Center the live preview
if (size.x < size.y)
{
double ScaleFactor = ThumbnailSize / (double)size.y;
int scaledX = (int)(size.x * ScaleFactor);
int xOffset = (ThumbnailSize - scaledX) / 2;
props.rcDestination.Left += xOffset;
props.rcDestination.Right -= xOffset;
}
if (size.y < size.x)
{
double ScaleFactor = ThumbnailSize / (double)size.x;
int scaledY = (int)(size.y * ScaleFactor);
int yOffset = (ThumbnailSize - scaledY) / 2;
props.rcDestination.Top += yOffset;
props.rcDestination.Bottom -= yOffset;
}
DwmUpdateThumbnailProperties(thumbnail, ref props);
}
}
For each available window we are going to choose a region in which its preview can be displayed. Basically, the preview region is a grid (filled from left to right, top to bottom) where each cell is a preview.
Known Issues
The application offered as download is not perfect and has a series of inconveniences. They are not big problems but I couldn’t find the time to fix them.
- If the list of available windows is modified, the update is not reflected in the application.
- Previews are no longer centered if you change the size of the window they represent.
- There should be a “glow” effect behind the text on glass in order to be visible on any surface. Just like in the Alt-Tab window.
- If clicking an application in the preview list, the focus is not transferred to it.