48

There used to be a way to get the active tab's URL from Google Chrome by using FindWindowEx in combination with a SendMessage call to get the text currently in the omnibox. A recent (?) update seems to have broken this method, since Chrome seems to be rendering everything itself now. (You can check with Spy++, AHK Window Spy or Window Detective)

To get the current URL on Firefox and Opera, you can use DDE and WWW_GetWindowInfo. This doesn't seem to be possible on Chrome (anymore?).

This question has an answer with more info about how it used to work, which is this piece of code (which, as I explained, doesn't work anymore - hAddressBox is 0):

var hAddressBox = FindWindowEx(
    intPtr,
    IntPtr.Zero,
    "Chrome_OmniboxView",
    IntPtr.Zero);

var sb = new StringBuilder(256);
SendMessage(hAddressBox, 0x000D, (IntPtr)256, sb);
temp = sb.ToString();

So my question is: Is there a new way to get the currently focused tab's URL? (Just the title is not enough)

Community
  • 1
  • 1
Codecat
  • 2,213
  • 3
  • 28
  • 39
  • Try looking at [this SO question](http://stackoverflow.com/questions/16841965/how-to-get-the-current-url-from-chrome-28-from-another-windows-application). – Icemanind Sep 23 '13 at 18:01

9 Answers9

46

Edit: Seems like the code in my answer here does not work anymore (though the idea of using AutomationElement does still work) for the later Chrome versions, so look through the other answers for different versions. For example, here's one for Chrome 54: https://stackoverflow.com/a/40638519/377618

The following code seems to work, (thanks to icemanind's comment) but is however resource intensive. It takes about 350ms to find elmUrlBar... a little slow.

Not to mention that we have the problem of working with multiple chrome processes running at the same time.

// there are always multiple chrome processes, so we have to loop through all of them to find the
// process with a Window Handle and an automation element of name "Address and search bar"
Process[] procsChrome = Process.GetProcessesByName("chrome");
foreach (Process chrome in procsChrome) {
  // the chrome process must have a window
  if (chrome.MainWindowHandle == IntPtr.Zero) {
    continue;
  }

  // find the automation element
  AutomationElement elm = AutomationElement.FromHandle(chrome.MainWindowHandle);
  AutomationElement elmUrlBar = elm.FindFirst(TreeScope.Descendants,
    new PropertyCondition(AutomationElement.NameProperty, "Address and search bar"));

  // if it can be found, get the value from the URL bar
  if (elmUrlBar != null) {
    AutomationPattern[] patterns = elmUrlBar.GetSupportedPatterns();
    if (patterns.Length > 0) {
      ValuePattern val = (ValuePattern)elmUrlBar.GetCurrentPattern(patterns[0]);
      Console.WriteLine("Chrome URL found: " + val.Current.Value);
    }
  }
}

Edit: I wasn't happy with the slow method above, so I made it faster (now 50ms) and added some URL validation to make sure we got the correct URL instead of something the user might be searching for on the web, or still being busy typing in the URL. Here's the code:

// there are always multiple chrome processes, so we have to loop through all of them to find the
// process with a Window Handle and an automation element of name "Address and search bar"
Process[] procsChrome = Process.GetProcessesByName("chrome");
foreach (Process chrome in procsChrome) {
  // the chrome process must have a window
  if (chrome.MainWindowHandle == IntPtr.Zero) {
    continue;
  }

  // find the automation element
  AutomationElement elm = AutomationElement.FromHandle(chrome.MainWindowHandle);

  // manually walk through the tree, searching using TreeScope.Descendants is too slow (even if it's more reliable)
  AutomationElement elmUrlBar = null;
  try {
    // walking path found using inspect.exe (Windows SDK) for Chrome 31.0.1650.63 m (currently the latest stable)
    var elm1 = elm.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Google Chrome"));
    if (elm1 == null) { continue; } // not the right chrome.exe
    // here, you can optionally check if Incognito is enabled:
    //bool bIncognito = TreeWalker.RawViewWalker.GetFirstChild(TreeWalker.RawViewWalker.GetFirstChild(elm1)) != null;
    var elm2 = TreeWalker.RawViewWalker.GetLastChild(elm1); // I don't know a Condition for this for finding :(
    var elm3 = elm2.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, ""));
    var elm4 = elm3.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ToolBar));
    elmUrlBar = elm4.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Custom));
  } catch {
    // Chrome has probably changed something, and above walking needs to be modified. :(
    // put an assertion here or something to make sure you don't miss it
    continue;
  }

  // make sure it's valid
  if (elmUrlBar == null) {
    // it's not..
    continue;
  }

  // elmUrlBar is now the URL bar element. we have to make sure that it's out of keyboard focus if we want to get a valid URL
  if ((bool)elmUrlBar.GetCurrentPropertyValue(AutomationElement.HasKeyboardFocusProperty)) {
    continue;
  }

  // there might not be a valid pattern to use, so we have to make sure we have one
  AutomationPattern[] patterns = elmUrlBar.GetSupportedPatterns();
  if (patterns.Length == 1) {
    string ret = "";
    try {
      ret = ((ValuePattern)elmUrlBar.GetCurrentPattern(patterns[0])).Current.Value;
    } catch { }
    if (ret != "") {
      // must match a domain name (and possibly "https://" in front)
      if (Regex.IsMatch(ret, @"^(https:\/\/)?[a-zA-Z0-9\-\.]+(\.[a-zA-Z]{2,4}).*$")) {
        // prepend http:// to the url, because Chrome hides it if it's not SSL
        if (!ret.StartsWith("http")) {
          ret = "http://" + ret;
        }
        Console.WriteLine("Open Chrome URL found: '" + ret + "'");
      }
    }
    continue;
  }
}
Community
  • 1
  • 1
Codecat
  • 2,213
  • 3
  • 28
  • 39
  • HI , i am using your code above but cannot figure out what is AutomationPattern[] and TreeScope . I cannot compile do i need to add some dll to use them / – confusedMind Oct 23 '13 at 23:39
  • 2
    Add `using System.Windows.Automation;` – Codecat Oct 24 '13 at 00:52
  • I did it works only for AutomationElement the other i mentioned above still remain unfound. – confusedMind Oct 24 '13 at 09:18
  • 1
    Here's a tip. In Visual Studio 2010, you can put your cursor on the unknown identifier, then press Ctrl+Dot on your keyboard. It will give you a list of things you can do to make it work. Anyway, you might need to include `UIAutomationTypes.dll` in your projects references. – Codecat Oct 24 '13 at 12:38
  • Thank you i did not know that :) will surely save time next time. – confusedMind Oct 24 '13 at 21:53
  • This will not work if Chrome is installed in another language. Do you have any ideas to solve it in any language? – Nelson Reis Nov 22 '13 at 20:16
  • @NelsonReis I have edited my answer, which works for different languages on my machine. – Codecat Nov 24 '13 at 22:15
  • 7
    Add the following two references to your solution 1.UIAutomationClient, 2.UIAutomationTypes. You will not get any errors.. – Rama Subba Reddy M Nov 27 '13 at 10:58
  • I can confirm this works for the latest version still at 31.0.1650.63 m. Also added a comment on how to detect if Incognito mode is enabled or disabled. – Codecat Jan 05 '14 at 00:54
  • 3
    Hi, This used to work quite nicely for me, but recently, with an upgrade of Chrome to version 34.0.1847.116m, it has stopped working, as Google have changed something. Can someone please advise of any fixes and/or tools that can be used to find the Handles and Properties, etc. so that this can be fixed? – QuietLeni Apr 10 '14 at 09:08
  • I will look into it soon. – Codecat Apr 12 '14 at 16:33
13

As of Chrome 54, the following code is working for me:

public static string GetActiveTabUrl()
{
  Process[] procsChrome = Process.GetProcessesByName("chrome");

  if (procsChrome.Length <= 0)
    return null;

  foreach (Process proc in procsChrome)
  {
    // the chrome process must have a window 
    if (proc.MainWindowHandle == IntPtr.Zero)
      continue;

    // to find the tabs we first need to locate something reliable - the 'New Tab' button 
    AutomationElement root = AutomationElement.FromHandle(proc.MainWindowHandle);
    var SearchBar = root.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "Address and search bar"));
    if (SearchBar != null)
      return (string)SearchBar.GetCurrentPropertyValue(ValuePatternIdentifiers.ValueProperty);
  }

  return null;
}
dotNET
  • 33,414
  • 24
  • 162
  • 251
  • Have you tried this for Chrome 55? Doesn't seem to work there. The way it's getting harder to get this seems to point at a deliberate effort to remove all access to the URL – Jon Limjap Dec 08 '16 at 02:41
  • Interesting. I wonder if it has something to do with the fact that I'm running on Parallels on a Mac. The searchBar returns a null for me in all cases. – Jon Limjap Dec 08 '16 at 05:22
  • @JonLimjap: I don't have access to a Mac right now and haven't tested it on that platform. Could have been differences at Chrome level (unlikely) or in Parallel's support of Automation library. Did you succeed with any other version of Chrome with this method? – dotNET Dec 08 '16 at 05:52
  • Verified on on 59.0.3071.86 and its previous version :) But it is taking almost 1013ms. – Abdul Rauf Jun 06 '17 at 17:43
  • Once we have this the cpu will shoot up to 80-90% for both firefox and chrome – jan_kiran Jul 03 '18 at 14:03
10

All the methods above are failing for me with Chrome V53 and above.

Here's what is working:

Process[] procsChrome = Process.GetProcessesByName("chrome");
foreach (Process chrome in procsChrome)
{
    if (chrome.MainWindowHandle == IntPtr.Zero)
        continue;

    AutomationElement element = AutomationElement.FromHandle(chrome.MainWindowHandle);
    if (element == null)
        return null;
    Condition conditions = new AndCondition(
        new PropertyCondition(AutomationElement.ProcessIdProperty, chrome.Id),
        new PropertyCondition(AutomationElement.IsControlElementProperty, true),
        new PropertyCondition(AutomationElement.IsContentElementProperty, true),
        new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit));

    AutomationElement elementx = element.FindFirst(TreeScope.Descendants, conditions);
    return ((ValuePattern)elementx.GetCurrentPattern(ValuePattern.Pattern)).Current.Value as string;
}

Found it here:

https://social.msdn.microsoft.com/Forums/vstudio/en-US/93001bf5-440b-4a3a-ad6c-478a4f618e32/how-can-i-get-urls-of-open-pages-from-chrome-and-firefox?forum=csharpgeneral

Randall Deetz
  • 512
  • 4
  • 25
7

I got results for Chrome 38.0.2125.10 with the next code (the code inside the 'try' block has to be replaced with this)

var elm1 = elm.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Google Chrome"));
if (elm1 == null) { continue; }  // not the right chrome.exe
var elm2 = TreeWalker.RawViewWalker.GetLastChild(elm1);
var elm3 = elm2.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.HelpTextProperty, "TopContainerView"));
var elm4 = elm3.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ToolBar));
var elm5 = elm4.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.HelpTextProperty, "LocationBarView"));
elmUrlBar = elm5.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit));
Paul3k
  • 166
  • 1
  • 4
5

I took Angelo's solution and cleaned it up a bit... I have a fixation with LINQ :)

This is the main method as it were; it uses a couple of extension methods:

public IEnumerable<string> GetTabs()
{
  // there are always multiple chrome processes, so we have to loop through all of them to find the
  // process with a Window Handle and an automation element of name "Address and search bar"
  var processes = Process.GetProcessesByName("chrome");
  var automationElements = from chrome in processes
                           where chrome.MainWindowHandle != IntPtr.Zero
                           select AutomationElement.FromHandle(chrome.MainWindowHandle);

  return from element in automationElements
         select element.GetUrlBar()
         into elmUrlBar
         where elmUrlBar != null
         where !((bool) elmUrlBar.GetCurrentPropertyValue(AutomationElement.HasKeyboardFocusProperty))
         let patterns = elmUrlBar.GetSupportedPatterns()
         where patterns.Length == 1
         select elmUrlBar.TryGetValue(patterns)
         into ret
         where ret != ""
         where Regex.IsMatch(ret, @"^(https:\/\/)?[a-zA-Z0-9\-\.]+(\.[a-zA-Z]{2,4}).*$")
         select ret.StartsWith("http") ? ret : "http://" + ret;
}

Note that the comment is misleading, as comments tend to be - it doesn't actually look at a single AutomationElement. I left it there because Angelo's code had it.

Here's the extension class:

public static class AutomationElementExtensions
{
  public static AutomationElement GetUrlBar(this AutomationElement element)
  {
    try
    {
      return InternalGetUrlBar(element);
    }
    catch
    {
      // Chrome has probably changed something, and above walking needs to be modified. :(
      // put an assertion here or something to make sure you don't miss it
      return null;
    }
  }

  public static string TryGetValue(this AutomationElement urlBar, AutomationPattern[] patterns)
  {
    try
    {
      return ((ValuePattern) urlBar.GetCurrentPattern(patterns[0])).Current.Value;
    }
    catch
    {
      return "";
    }
  }

  //

  private static AutomationElement InternalGetUrlBar(AutomationElement element)
  {
    // walking path found using inspect.exe (Windows SDK) for Chrome 29.0.1547.76 m (currently the latest stable)
    var elm1 = element.FindFirst(TreeScope.Children,
      new PropertyCondition(AutomationElement.NameProperty, "Google Chrome"));
    var elm2 = TreeWalker.RawViewWalker.GetLastChild(elm1); // I don't know a Condition for this for finding :(
    var elm3 = elm2.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, ""));
    var elm4 = elm3.FindFirst(TreeScope.Children,
      new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ToolBar));
    var result = elm4.FindFirst(TreeScope.Children,
      new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Custom));

    return result;
  }
}
Marcel Popescu
  • 3,146
  • 3
  • 35
  • 42
5

I discovered this post and was able to successfully pull the URL from chrome in C# using these methods, thank you everyone!

Unfortunately with the recent Chrome 69 update, the AutomationElement tree traversal broke again.

I found this article by Microsoft: Navigate Among UI Automation Elements with TreeWalker

And used it to whip up a simple function that searches for the AutomationElement with the "edit" control type we're looking for, instead of traversing a tree heirarchy that is always changing, and from there extract the url Value from that AutomationElement.

I wrote a simple class that wraps this all up: Google-Chrome-URL-Check-C-Sharp.

The readme explains a bit on how to use it.

All in all it might just be a little more reliable, and hope some of you find it useful.

progietheus
  • 76
  • 1
  • 3
  • 1
    I've just added 2 small fixes to your class in github. Thank you very much. Good work! (it works even with old chrome 49). – Fil Dec 06 '18 at 13:40
4

Refering to the solution of Angelo Geels, here is a patch for version 35 - the code inside the "try" block has to be replaced with this:

var elm1 = elm.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Google Chrome"));
if (elm1 == null) { continue; } // not the right chrome.exe
var elm2 = TreeWalker.RawViewWalker.GetLastChild(elm1); // I don't know a Condition for this for finding
var elm3 = elm2.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, ""));
var elm4 = TreeWalker.RawViewWalker.GetNextSibling(elm3); // I don't know a Condition for this for finding
var elm7 = elm4.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ToolBar));
elmUrlBar = elm7.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Custom));  

I took it from here: http://techsupt.winbatch.com/webcgi/webbatch.exe?techsupt/nftechsupt.web+WinBatch/dotNet/System_CodeDom+Grab~URL~from~Chrome.txt

Michal Czardybon
  • 2,795
  • 4
  • 28
  • 40
4

For me only the active chrome window has a MainWindowHandle. I got around this by looking through all windows for chrome windows, and then using those handles instead. For example:

    public delegate bool Win32Callback(IntPtr hwnd, IntPtr lParam);

    [DllImport("user32.dll")]
    protected static extern bool EnumWindows(Win32Callback enumProc, IntPtr lParam); 

    private static bool EnumWindow(IntPtr handle, IntPtr pointer)
    {
        List<IntPtr> pointers = GCHandle.FromIntPtr(pointer).Target as List<IntPtr>;
        pointers.Add(handle);
        return true;
    }

    private static List<IntPtr> GetAllWindows()
    {
        Win32Callback enumCallback = new Win32Callback(EnumWindow);
        List<IntPtr> pointers = new List<IntPtr>();
        GCHandle listHandle = GCHandle.Alloc(pointers);
        try
        {
            EnumWindows(enumCallback, GCHandle.ToIntPtr(listHandle));
        }
        finally
        {
            if (listHandle.IsAllocated) listHandle.Free();
        }
        return pointers;
    }

And then to get all chrome windows:

    [DllImport("User32", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int GetWindowText(IntPtr windowHandle, StringBuilder stringBuilder, int nMaxCount);

    [DllImport("user32.dll", EntryPoint = "GetWindowTextLength", SetLastError = true)]
    internal static extern int GetWindowTextLength(IntPtr hwnd);
    private static string GetTitle(IntPtr handle)
    {
        int length = GetWindowTextLength(handle);
        StringBuilder sb = new StringBuilder(length + 1);
        GetWindowText(handle, sb, sb.Capacity);
        return sb.ToString();
    }

and finally:

GetAllWindows()
    .Select(GetTitle)
    .Where(x => x.Contains("Google Chrome"))
    .ToList()
    .ForEach(Console.WriteLine);

Hopefully this saves someone else some time in figuring out how to actually get the handles of all the chrome windows.

yeerk
  • 2,103
  • 18
  • 16
  • This actually gets any window that contains "Google Chrome" in the title. (e.g. open this webpage in Internet Explorer.) Also, for some reason, the returned list has an extra element... – Derek Johnson Jun 20 '15 at 07:32
  • Yes, but once you run the algorithms mentioned in the other answers you can figure out which windows are chrome based on if you can or cannot get a url. Any search being more inclusive is okay, for me the problem was the search being too exclusive (only giving me processes of focused chrome windows). – yeerk Jun 21 '15 at 15:54
1

For version 53.0.2785 got it working with this:

var elm1 = elm.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Google Chrome"));
                if (elm1 == null) { continue; } // not the right chrome.exe
                var elm2 = elm1.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, ""))[1];
                var elm3 = elm2.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, ""))[1];
                var elm4 = elm3.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "principal"));
                var elm5 = elm4.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, ""));
                elmUrlBar = elm5.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit));
Yind
  • 335
  • 3
  • 17