Collecting URLs

Another one of those itches that need to be scratched: someone posted a query internally asking for tools that would copy URLs for all open browser windows to the clipboard. I don't know if such a tool exists but I thought it would be a fun exercise to write one regardless, especially since I had chunks of the necessary code hiding in Project Colletta and its test harness already.

The first part of the puzzle is to find all the browser windows (tabs, actually) - I'm going to restrict myself to Internet Explorer because there's a convenient mechanism for locating them all (and, beside, given who I work for, it's obvious where I'm going to start), IShellWindows, if I get hold of an appropriate object exposing that interface, I can iterate over the collection, identifying which windows are actually browser windows (the collection includes Windows Explorer as well as Internet Explorer), and grab their URLs:

         private static IEnumerable<Tuple<string, string>> GetAllInternetExplorerTabs()
        {
            var tabs = new List<Tuple<string, string>>();

            var CLSID_ShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39");
            var shellWindowsType = Type.GetTypeFromCLSID(CLSID_ShellWindows);
            dynamic shellWindows = Activator.CreateInstance(shellWindowsType);

            int count = shellWindows.Count;
            for (int i = 0; i < count; i++)
            {
                try
                {
                    var browser = shellWindows.Item[i];
                    if (browser != null)
                    {
                        if (GetClassName((IntPtr)browser.HWND) == "IEFrame")
                        {
                            string url = browser.LocationURL;
                            string title = browser.LocationName;
                            Marshal.ReleaseComObject(browser);
                            tabs.Add(new Tuple<string, string>(url, title));
                        }
                    }
                }
                catch
                {
                }
            }
            Marshal.ReleaseComObject(shellWindows);
            return tabs;
        }

Ignoring the fact that I'm being embarrassingly cavalier with regard to exception handling here, this operates by grabbing a ShellWindows object and extracting the URL and title from found browser windows. I've managed to keep the code very short by (lazily) relying on "dynamic" to look up properties and methods instead of COM casting things to the correct types. There will be a performance cost, but it's not important for this trivial application. If any of the dynamic accesses fail, an exception is thrown, hence my try...catch. I identify IE windows by looking at their window class names, using the helper function below. Finally, this routine returns a list of URL and title pairs for further processing.

         private static string GetClassName(IntPtr hwnd)
        {
            var sb = new StringBuilder(256);
            GetClassName(hwnd, sb, sb.Capacity);
            return sb.ToString();
        }

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern int GetClassName(IntPtr hwnd, StringBuilder lpString, int nMaxCount);

.NET offers a very easy way to place matter on the clipboard, Clipboard.SetText, and I use it as follows:

         private static void CopyInternetExplorerTabsToClipboard()
        {
            var sb = new StringBuilder();
            foreach (var t in GetAllInternetExplorerTabs())
            {
                if (Properties.Settings.Default.IncludeTitles)
                    sb.AppendLine(t.Item2);
                sb.AppendLine(t.Item1);
            }

            if (sb.Length > 0)
                Clipboard.SetText(sb.ToString());
        }

I found I needed that length check because Clipboard.SetText got unhappy when given no text.

The next step is to wrap this in an application - I created a Windows Forms app, and threw away the form, creating a Notification Area icon instead:

         private static NotifyIcon CreateNotificationIcon()
        {
            var menu = new ContextMenu(
                new MenuItem[]{
                    new MenuItem("Clip URLs", (sender, e) => { CopyInternetExplorerTabsToClipboard(); }),
                    new MenuItem("Include titles", (sender, e) =>
                                                {
                                                    var mi = (MenuItem)sender;
                                                    Properties.Settings.Default.IncludeTitles = !mi.Checked;
                                                    Properties.Settings.Default.Save();
                                                    mi.Checked = Properties.Settings.Default.IncludeTitles;
                                                }){ Checked = Properties.Settings.Default.IncludeTitles },
                    new MenuItem("-"),
                    new MenuItem("Exit", (sender, ee) => { Application.Exit(); })
                });
            var notify = new NotifyIcon
            {
                Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location),
                ContextMenu = menu,
                Text = "Browser URL clipper",
                Visible = true
            };
            notify.Click += (sender, e) => { CopyInternetExplorerTabsToClipboard(); };

            return notify;
        }

Note that I added a boolean property, IncludeTitles, to the project settings to preserve the user's choice of whether to, er, include titles in the clipped text or not. A left click on the icon will clip all URLs, a right click pops up a menu which offers control of that property, and an application exit option.

Now, it is a bit of a chore to have to move the mouse pointer all the way to the Notification Area, so I added a basic hotkey handler too. This would have been easier if my application had a main window, but it's only slightly more effort without - I need to install a message filter, which gets a chance to peek at all messages sent to the main thread, and traps my hotkey message:

         private const int HotkeyId = 1196; // Totally arbitrary

        private class MessageFilter : IMessageFilter
        {
            public bool PreFilterMessage(ref Message m)
            {
                if (m.Msg == 0x0312 /* WM_HOTKEY */ && m.WParam.ToInt32() == HotkeyId)
                {
                    CopyInternetExplorerTabsToClipboard();
                    return true;
                }
                return false;
            }
        }

With all that in place, all that remains is to register the hotkey, create the notification icon, install the message filter, and run the message pump. When the user exits the application, undo all of that too:

         [STAThread]
        static void Main()
        {
            RegisterHotKey(IntPtr.Zero, HotkeyId, 6 /* Ctrl + Shift */, 'C');
            var notify = CreateNotificationIcon();
            var filter = new MessageFilter();
            Application.AddMessageFilter(filter);

            Application.Run();

            Application.RemoveMessageFilter(filter);
            notify.Dispose();
            UnregisterHotKey(IntPtr.Zero, HotkeyId);
        }

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool RegisterHotKey(IntPtr hwnd, int id, int modifiers, uint vk);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnregisterHotKey(IntPtr hwnd, int id);

I've arbitrarily chosen Ctrl+Shift+C to snapshot URLs - if I wanted to be less lazy, I'd include a configuration option for this, but I'll leave it as an exercise for the reader.

That's it: about 100 lines of code to grab URLs from all current Internet Explorer tabs and drop them in the clipboard. Sometimes .NET programming is very satisfying!