Contents
This program allows to host several open Windows in one parent window so that you can easily access and navigate between them, as well as clean up space in the taskbar. The idea of creating this program came to me when I was reading an article by Jay Nelson: Hosting EXE Applications in a WinForm project. Instead of hosting just a single executable inside a WinForm project, I decided to have a tabcontrol and host different Windows on different tabs. This will allow to group similar Windows together, easily navigate between them and clean up space in the taskbar. Interested? In that case, let's start exploring the application.
All the functionality of this application is achieved using Windows API functions, so you should be familiar with basic winapi
programming. Consequently you should know what P/Invoke is and how it works. If you are familiar with the article: Window Tray Minimizer, then it will help you a lot.
When you run the application, it hides the main form and an icon is shown in the system tray. If you right-click the icon, a context menu will be shown which has two main buttons: 'Tab Windows' and 'Tab all Windows'. When you click 'Tab Windows', a new window will pop up with a list of open Windows. The list can be filtered by the title of the Windows. When you select the Windows you wish to tabify and click 'OK', a new window will be created and all the checked Windows will be hosted in a tabcontrol. There will be one more tabpage called Menu which has two buttons: one for adding open Windows to the tabcontrol and another for choosing files that will be automatically opened in new tabs. You can also drag & drop files or folders from Windows Explorer on this tabpage for having them opened in new tabs automatically. You can navigate between tabs by clicking Ctrl+1, Ctrl+2 and so on or you can just simply hover mouse over the tab and it will be selected automatically. When selected tab changes, icon of the host window is changed either with the icon of the executable file that created current window or with the icon of the window itself. Minimizing the host window minimizes it to tray, but if it is not what you want, you can turn off the feature from the options dialog.
Before we begin exploring the application itself, I'd like to introduce a class for storing simple properties of a window and methods for manipulation on it. The main properties are: Handle
, Location
, Parent
, Size
and Title
. The main methods include closing the window, getting the path of the executable file that created the window, moving it, setting/restoring parent and setting style. There is also one static
method that enumerates all open Windows. The class has one constructor that takes the window's handle as a parameter and sets its simple properties. Here is a class diagram:
When you click the 'Tab Windows' button, a window is shown which lists all open Windows. In order to enumerate all Windows, you should call the winapi
function called EnumWindows
. The function takes two parameters. The first one is a pointer to a callback function. The code snippet below shows how this function works:
public static List<window> GetOpenWindows()
{
openwnd = new List<window>();
winapi.EnumWindowsProc callback = new winapi.EnumWindowsProc(EnumWindows);
winapi.EnumWindows(callback, 0);
List<window> result = new List<window>(openwnd);
openwnd.Clear();
result.RemoveAt(result.Count - 1);
return result;
}
private static bool EnumWindows(IntPtr hWnd, int lParam)
{
if (!winapi.IsWindowVisible(hWnd) || hWnd == winapi.statusbar)
return true;
openwnd.Add(new window(hWnd));
return true;
}
After this, we need to filter this list. Firstly, we need to get rid of the Windows that were opened by our application so that we don't get host Windows hosting other host Windows. This can be done for each returned window by finding the path of the executable that created the window and comparing it to our application's location. Secondly, if the user has selected to ignore Windows without a title, we have to remove them.
These are the steps required to find the path of the executable that created a given window:
- Get the process id that created the specified window using the
GetWindowThreadProcessId()
function
- Get a handle of the process by
OpenProcess()
function
- Get the executable path by calling
GetModuleFileNameEx()
function
All these functions are winapi
functions imported by dllimport
attribute. Here is the actual implementation ported to C#:
public string GetExecutablePath()
{
uint dwProcessId;
winapi.GetWindowThreadProcessId(handle, out dwProcessId);
IntPtr hProcess = winapi.OpenProcess(winapi.ProcessAccessFlags.VMRead |
winapi.ProcessAccessFlags.QueryInformation, false, dwProcessId);
StringBuilder path = new StringBuilder(1024);
winapi.GetModuleFileNameEx(hProcess, IntPtr.Zero, path, 1024);
winapi.CloseHandle(hProcess);
return path.ToString();
}
At this point we have a variable of List<window>
class. In C# 2.0, we can use FindAll
method of List<T>
class to filter it.
private void GetWindows()
{
if (Properties.Settings.Default.Ignore)
{
windows = window.GetOpenWindows().FindAll(delegate(window wnd) {
return wnd.Title.Length > 0 && wnd.GetExecutablePath() != Application.ExecutablePath; });
}
else
{
windows = window.GetOpenWindows().FindAll(delegate(window wnd) {
return wnd.GetExecutablePath() != Application.ExecutablePath; });
}
}
In C# 3.0, you can make use of a new feature called Lambda expressions and rewrite it like this:
private void GetWindows()
{
if (Properties.Settings.Default.Ignore)
{
windows = window.GetOpenWindows().FindAll((window wnd)=>wnd.Title.Length>0 &&
wnd.GetExecutablePath()!=Application.ExecutablePath);
}
else
{
windows = window.GetOpenWindows().FindAll((window wnd) => wnd.GetExecutablePath()
!= Application.ExecutablePath);
}
}
When a user selects those Windows that are to be tabbed and clicks OK, a new 'host' window is created and selected Windows are passed to it. When the host is displayed, it adds a new tabpage for each window and displays the window.
private void ProcessWindows(List<window> windows)
{
lock (tabs)
{
int startindex = tabs.Items.Count - 1;
for (int i = startindex; i < windows.Count; i++)
{
int count = tabs.Items.Add(new FATabStripItem(windows[i].Title, null));
windows[i].SetParent(tabs.Items[count].Handle);
windows[i].SetStyle(winapi.GWL_STYLE, (IntPtr)winapi.WS_VISIBLE);
windows[i].Move(tabs.Location, tabs.Size, true);
}
}
}
Whenever a tabpage is closed, the window that was displayed on it is released.
private void Release(window wnd)
{
wnd.RestoreParent();
wnd.SetStyle(winapi.GWL_STYLE, (IntPtr)wnd.PreviousStyle);
wnd.Move(wnd.Location, wnd.Size, true);
}
Detecting drag 'n' drop of files from Windows Explorer on the menu tab is detected by the component that comes with the source code of this book: Windows Forms 2.0 Programming. When files or folders are dropped on the form or user selects them by clicking 'Open files in new tab', they are filtered and a new process is started using the filename.
private void ProcessFiles(string[] files)
{
foreach (string filename in files)
{
if (!filename.EndsWith(".lnk"))
{
ParameterizedThreadStart thrparam = new ParameterizedThreadStart(ProcessFile);
Thread thr = new Thread(thrparam);
thr.Start(filename);
}
}
}
The ProcessFile
method starts a new process based on a parameter, waits 5 seconds for an application to become idle and then checks its MainWindowHandle
property. If a folder was dropped, then it tries to get the handle to the window which was created by using winapi FindWindow
function.
private void ProcessFile(object filename)
{
string path = filename as string;
if (File.Exists(path))
{
Process proc = Process.Start(path);
if (proc != null)
{
proc.WaitForInputIdle(5000);
if (proc.MainWindowHandle != IntPtr.Zero)
{
lock (hostedwindows)
{
hostedwindows.Add(new window(proc.MainWindowHandle));
}
}
proc.Dispose();
}
}
else
if (Directory.Exists(path))
{
int i = 0;
Process.Start(path);
IntPtr handle = IntPtr.Zero;
while (handle==IntPtr.Zero && i<5)
{
i++;
Thread.Sleep(1000);
handle = window.FindWindow("CabinetWClass", Path.GetFileName(path));
}
if (handle != IntPtr.Zero)
{
lock (hostedwindows)
{
hostedwindows.Add(new window(handle));
}
}
}
}
Except just clicking the tab with mouse which you wish to select there are two ways to navigate between them: You can either click Ctrl+1, Ctrl+2, etc. at the same time to switch to the corresponding tab or you can simply hover mouse over the tab and it will be selected automatically. The code snippets below show how these are accomplished.
Code Snippet for Ctrl+1, Ctrl+2, etc.
private void tabs_KeyDown(object sender, KeyEventArgs e)
{
if (e.Control && e.KeyValue>48 && e.KeyValue<58 && tabs.Items.Count>=(e.KeyValue-48))
{
tabs.SelectedItem = tabs.Items[e.KeyValue - 49];
}br>}
Code Snippet for Automatically Selecting Tab when Mouse is Moved Over It
private void tabs_MouseMove(object sender, MouseEventArgs e)
{
FATabStripItem c = tabs.GetTabItemByPoint(e.Location);
if (c != null && Properties.Settings.Default.SelectonHover)
{
tabs.SelectedItem = tabs.Items[tabs.Items.IndexOf(c)];
}
}
When you change active tab in host window, the host window icon changes either with the icon of the executable file that created the currently displayed window or with the icon of the window itself that is displayed. So we need to retrieve either the executable icon or the window's icon.
In order to retrieve the executable icon we need to call SHGetFileInfo()
and pass the path of the executable file and an instance of SHFILEINFO
structure. After you have retrieved a handle to the icon, you must call DestoyIcon()
API to prevent a memory leak.
private Icon GetIcon()
{
System.Drawing.Icon icon = null;
string path = GetExecutablePath();
if (System.IO.File.Exists(path))
{
winapi.SHFILEINFO info = new winapi.SHFILEINFO();
winapi.SHGetFileInfo(path, 0, ref info, (uint)Marshal.SizeOf(info),
winapi.SHGFI_ICON | winapi.SHGFI_SMALLICON);
System.Drawing.Icon temp = System.Drawing.Icon.FromHandle(info.hIcon);
icon = (System.Drawing.Icon)temp.Clone();
winapi.DestroyIcon(temp.Handle);
}
return icon;
}
In order to retrieve window icon, I used the code snippet from this article: Screen Captures, Window Captures and Window Icon Captures with Spy++ Style Window Finder! with small modifications. Here it is:
private Icon GetWindowIcon()
{
int result;
winapi.SendMessageTimeout(handle, winapi.WM_GETICON, winapi.ICON_SMALL, 0,
winapi.SMTO_ABORTIFHUNG, 1000, out result);
IntPtr IconHandle = new IntPtr(result);
if (IconHandle == IntPtr.Zero)
{
result = winapi.GetClassLong(handle, winapi.GCL_HICONSM);
IconHandle = new IntPtr(result);
}
if (IconHandle == IntPtr.Zero)
{
winapi.SendMessageTimeout(handle, winapi.WM_QUERYDRAGICON, 0, 0,
winapi.SMTO_ABORTIFHUNG, 1000, out result);
IconHandle = new IntPtr(result);
}
if (IconHandle == IntPtr.Zero)
{
return null;
}
System.Drawing.Icon temp = System.Drawing.Icon.FromHandle(IconHandle);
System.Drawing.Icon icon = (System.Drawing.Icon)temp.Clone();
winapi.DestroyIcon(IconHandle);
return icon;
}
You can add the program to start-up from the options Window. To add program to start-up, you need to navigate to HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run key, create a new string
and set its value equal to the application's path. Removing the program from start-up is easier: you just remove the value. The code snippet below shows how to do it:
[RegistryPermissionAttribute(SecurityAction.LinkDemand,
Write = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run")]
private void startup(bool add)
{
RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\
CurrentVersion\Run", true);
if (add)
{
key.SetValue("Window Tabifier", "\"" + Application.ExecutablePath + "\"");
}
else
key.DeleteValue("Window Tabifier");
key.Close();
}
If you try to launch the second instance of the application, you will get a message box saying that it is already running. This is achieved using the mutex
class. Mutex
allows to share resources between threads. When the first instance of the program is launched, it creates a new mutex
. When a second instance is launched, it checks the existence of the mutex
. If it exists, then it exits. When the first instance quits, it releases the existing mutex
.
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Mutex mt = null;
try
{
mt = Mutex.OpenExisting("Window Tabifier");
}
catch (WaitHandleCannotBeOpenedException)
{
}
if (mt == null)
{
mt = new Mutex(true, "Window Tabifier");
Application.Run(new Main());
GC.KeepAlive(mt);
mt.ReleaseMutex();
}
else
{
mt.Close();
MessageBox.Show("Application already running");
Application.Exit();
}
}
These are possible features that would make the application more useful:
- Detecting window opening automatically and adding it to the host window
- Detecting
WM_SETTEXT
message for tabbed Windows in order to update tab title.
Both features require setting Windows hooks.
While experimenting with this application, I found that if you start a new process through file shortcut, then the return value is always null
.
I would like to thank Giorgi Moniava for the advice he gave me.
- 20th January, 2008 - Initial release
- 11th February, 2008 - version 1.5
- Bug #1: When a tabbed window is released, it has the same state as before it was tabbed. (If the window was maximized before tabbing, it will be maximized after it is released.)
- Bug #2: When a minimized window is released, you no more need to right-click it with the mouse and click restore in order to open it, you can just click it with the mouse as you usually would.
- Feature #1: You can now drag and drop a folder on the host window and it will be automatically opened in a new tab.
- 6th March, 2008 - version 1.6
- Feature #1: A tab is selected automatically when the mouse is moved over it.
- 20th March, 2008 - version 1.8
- Feature #1: When you change the active tab in the host window, the host window icon is set to the icon of the executable file that created the currently displayed window.
- Feature #2: Host window minimizes to system tray. This feature can be turned off from the options dialog.
- 29th March, 2008 - version 1.9
- Feature #1: When you change the active tab in the host window, the host window icon changes either with the icon of the executable file that created the currently displayed window or with the icon of the window itself. This behaviour can be configured from the options dialog.