get a list of open tabs from chrome?

Started by kdmoyers, December 15, 2021, 02:48:07 PM

Previous topic - Next topic

kdmoyers

So, for years I have been yearning to be able to see the titles of all the chrome browser tabs, not just the currently displayed one.  The business case for me gets stronger every year.

(It's easy enough to select a tab by sending Ctrl-1,2,3,4 but without knowing which tab you're aiming for...)

I happened to stumble on this [1] stackoverflow page which seems to present a solution in a single page of c# code.  But I know next to nothing about c#. I quoted the code below.

QUESTION:
Does this look like something a moderate wbt hacker like me might be able to translate into WinBatch?  Or does it use something winbatch can't easily do?

[1] https://stackoverflow.com/questions/40070703/how-to-get-a-list-of-open-tabs-from-chrome-c-sharp

Code (csharp) Select
First Reference two dlls

UIAutomationClient.dll
UIAutomationTypes.dll

Located: C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0 (or 3.5)

Then

using System.Windows.Automation;

Process[] procsChrome = Process.GetProcessesByName("chrome");
if (procsChrome.Length <= 0)
{
   Console.WriteLine("Chrome is not running");
}
else
{
   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);
      Condition condNewTab = new PropertyCondition(AutomationElement.NameProperty, "New Tab");
      AutomationElement elmNewTab = root.FindFirst(TreeScope.Descendants, condNewTab);
 
      // get the tabstrip by getting the parent of the 'new tab' button
      TreeWalker treewalker = TreeWalker.ControlViewWalker;
      AutomationElement elmTabStrip = treewalker.GetParent(elmNewTab);
 
      // loop through all the tabs and get the names which is the page title
      Condition condTabItem = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TabItem);
      foreach (AutomationElement tabitem in elmTabStrip.FindAll(TreeScope.Children, condTabItem))
      {
          Console.WriteLine(tabitem.Current.Name);
      }
   }
}
The mind is everything; What you think, you become.

td

I don't use chrome nor even have it on any of our systems but the c# code you posted is certainly translatable into WIL. If that didn't work for some reason, you could always compile the c# into an in-memory assembly that WIL can use directly in the same script that performed the compile. There are multiple examples of this sort of thing on this forum. Alternatively, if Chrome happens to create a separate process for each tab you might be able to use straight WIL (without .Net) to perform the task.
"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

kdmoyers

OK, I'm gonna try it, wish me luck!
That bit about "reference two dlls" looked scary.

Best of all possible outcomes would be a pure wbt solution, using the regular dotNet
integration features, so that's what I'm going to try first.  This will be my first go at
dotNet stuff.  Woo!
The mind is everything; What you think, you become.

kdmoyers

I see Jim posted something really similar -- great ideas to copy from.  Thanks Jim!
Looks like he went with the compile-c#-in-memory option.
The mind is everything; What you think, you become.

JTaylor


td

Quote from: kdmoyers on December 16, 2021, 08:35:31 AM
I see Jim posted something really similar -- great ideas to copy from.  Thanks Jim!
Looks like he went with the compile-c#-in-memory option.

Here is a link to a .Net automation script without in-memory compilation I played with many moons ago. You may have already seen it and am not sure how useful it is for your application. But thought I would mention it anyway.

https://forum.winbatch.com/index.php?topic=1020.msg4696#msg4696
"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

td

"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

jmburton2001

In the military they have a saying... If you want a job performed quickly and efficiently, get the laziest private!

Soooo... from the perspective of one who's both lazy and a hobbyist, I'll just spitball here.

I know Chrome has a built-in "Task Manager" that has a concise list of all running processes. Every tab that's open in Chrome is prepended with the word "Tab" (kinda genius right?) in this task list. I was hoping that there was a simple way to export that list and then the resulting list could be parsed to get a current list of tabs.

Unfortunately there isn't a "quick and dirty" way to get that list BUT... evidently Chrome has an API system that will allow access to running processes, including tab specific information.

I haven't looked into it in detail, but that's probably where I'd start if I were trying to get a list of tabs using Winbatch.

As usual, this is just my two cents (most likely worth less) and YMMV   ;)

stanl

I could throw in some powershell, based off the C# code but even there was a warning it might be version specific. I also don't use chrome that much.

stanl

Well, couldn't resist ::) .  The code below is workable Powershell, but incredibly ugly and perhaps useless. However, with Kirby's remarks that he might want a pure WB solution.... well you know WB can run PS directly and the C# code conversion probably accomplishes the same results... but the interesting thing about the PS code is it utilizes Process Extender / shell COM Object... so the logic may be derivative to WB code..  AnyWho

       
  • Script looks for Chrome running process... or fails with Message
  • If process is running... takes the count
  • Since process is running, create shell script object to activate it
  • The issue is that Chrome Process will only display the active window or last opened window/tab....
  • So make subsequent iterative calls after having shell move the Tab
This will get a Rube Goldberg nod... but the logic might have applicability to raw WB code... dunno


Tested with 5 active Chrome windows




$result = ""
$prcs = Get-Process -Name chrome -ErrorAction SilentlyContinue
if ($prcs) {


$n = $prcs.Count
    #$brk = $false
    $wshell=New-Object -ComObject wscript.shell
$wshell.AppActivate('Chrome')
    for ($i=1;$i -lt $n;$i++) {
   Sleep 2
   $wshell.SendKeys('^{PGUP}')
   #$wshell.SendKeys('{F5}')
       $prcs1 = Get-Process -Name chrome
       foreach ($p in $prcs1) {
          if ($p.MainWindowTitle -ne '') {
             if ($result -match $p.MainWindowTitle) {
                #$brk = $true
                break }
             else {
                $result = $result +$p.MainWindowTitle + "`n" }
          }
       }
       #if ($brk) {break}
   }
}


else { $result = "Chrome is not running" }
$result



stanl

Quote from: stanl on December 18, 2021, 02:04:55 PM
Well, couldn't resist ::) .  The code below is workable Powershell


little cleaner, should stop excessive tab iterations...


# iterate tab titles in Chrome if active
$result = ""
$prcs = Get-Process -Name chrome -ErrorAction SilentlyContinue  # no error if chrome process doesn't exist
#chrome is running
if ($prcs) {
   $n = $prcs.Count  # should have several processes; hopefully covers all tabs
    $wshell=New-Object -ComObject wscript.shell
   $wshell.AppActivate('Chrome')
    for ($i=1;$i -lt $n;$i++) {
      Sleep 2
      $wshell.SendKeys('^{PGUP}') # advance tab from current
      #$wshell.SendKeys('{F5}')  # if you want to refresh url link
       $prcs1 = Get-Process -Name chrome # now redo updated process
       #script will page through tabs; collect tab titles
       foreach ($p in $prcs1) {
          if ($p.MainWindowTitle -ne '') {
             if ($result -notmatch $p.MainWindowTitle) {$result = $result +$p.MainWindowTitle + "`n" }
             else { $n = 1 }  # shoud break previous loop
          }
       }
   }
}
else { $result = "Chrome is not running" }
#just show titles, could be put into notepad nd displayed
$result



kdmoyers

Much to think about!  Thanks everyone.
-Kirby
The mind is everything; What you think, you become.

td

Didn't try this with Chrome so I have no idea if it will work with it but it does work with Firefox. Just plan old WIL code to think about.

Code (winbatch) Select
Name = WinItemizeEx('~Firefox',@True, @True, 0)
First = Name
FireList = Name
while Name != ''
   SendKeysTo(Name, '^{PGUP}', 1) ; Time delay spec may not be necessary??
   Name = WinItemizeEx('~Firefox',@True, @True, 0)
   if First == Name then break
   else FireList := @lf:name
EndWhile

Message('Firefox Tab Names', FireList)   


[Edit] The above does not handle the case of multiple instances of Firefox.
"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

kdmoyers

Yes, I did get the spin-thru-windows-to-learn-titles method to work, but it creates quite the visual spectacle for the user, and I don't think I could get away with it. 

I'm attempting the dotNet strategy, and have a question:  this code fails on the "root =" line:
Code (winbatch) Select
    ; Load required assemblies.
    ObjectClrOption("use","UIAutomationClient, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
    ObjectClrOption("use","UIAutomationTypes, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
    ObjectClrOption("useany","System")

    ;Process[] procsChrome = Process.GetProcessesByName("chrome");
    ;foreach (Process chrome in procsChrome)

    objProcess = ObjectClrNew("System.Diagnostics.Process")
    strProcName = "chrome"
    aProcs = objProcess.GetProcessesByName(strProcName)

    IntPtrZero = ObjectClrType("System.IntPtr",0)

    foreach proc in aProcs
      if proc.MainWindowHandle == IntPtrZero ; most are zero
        continue
      endif
      handle = ObjectClrType("System.IntPtr",proc.MainWindowHandle)

      ;AutomationElement root = AutomationElement.FromHandle(chrome.MainWindowHandle);
      objUiElement = ObjectClrNew("System.Windows.Automation.AutomationElement")
      testElement = objUiElement.RootElement ; this makes no error
      root = objUiElement.FromHandle(handle) ; this make an error

      ; fails: "Method  System.Windows.Automation.AutomationElement.FromHandle not found"
      ; but: https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.fromhandle
     
    next


For the life of me, I can't see why the call to FromHandle does not work.

The goal is to implement this C# code I found at
stackoverflow.com/questions/39164128/how-to-activate-a-google-chrome-tab-item-using-ui-automation
Code (csharp) Select
        Process[] procsChrome = Process.GetProcessesByName("chrome");
        foreach (Process chrome in procsChrome)
        {
            // the chrome process must have a window
            if (chrome.MainWindowHandle == IntPtr.Zero)
            {
                continue;
            }

            AutomationElement root = AutomationElement.FromHandle(chrome.MainWindowHandle);
           
            Condition condNewTab = new PropertyCondition(AutomationElement.NameProperty, "Nueva pestaña");
            AutomationElement elmNewTab = root.FindFirst(TreeScope.Descendants, condNewTab);
            // get the tabstrip by getting the parent of the 'new tab' button
            TreeWalker treewalker = TreeWalker.ControlViewWalker;
            AutomationElement elmTabStrip = treewalker.GetParent(elmNewTab);
             
            // loop through all the tabs and get the names which is the page title
            Condition condTabItem = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TabItem);
            foreach (AutomationElement tabitem in root.FindAll(TreeScope.Descendants, condTabItem))
            {
                Console.WriteLine(tabitem.Current.Name);

                // I NEED TO ACTIVATE THE TAB HERE

                break;
            }
The mind is everything; What you think, you become.

kdmoyers

Scratch that C# code, I have this new code which is verified to work, because I just ran it successfully.
Yes, it actually iterates thru the chrome tabs, in order!
It's also much clearer.  Still, I have to get that root= thing to work in winbatch before I can get the rest working in winbatch.

Code (csharp) Select
Process[] procsChrome = Process.GetProcessesByName("chrome");
foreach (Process chrome in procsChrome)
{
  // the chrome process must have a window
  if (chrome.MainWindowHandle == IntPtr.Zero)
  {
      continue;
  }

  AutomationElement root = AutomationElement.FromHandle(chrome.MainWindowHandle);
  var descTabs = root.FindAll(TreeScope.Descendants, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TabItem));
  foreach (AutomationElement item in descTabs)
  {
      var name = item.Current.Name;
  }   
}
The mind is everything; What you think, you become.

stanl

Hey Kirby;


Is your code you are working on getting just the Tab title, or does it expose the underlying url?  I say this as at work Chrome is used to open several SAP server environments via SSO.. Even though PS can easily iterate the Tabs, the same title is used for each sever and only the underlying url is the key. I think that would be a job for Selenium, but would be a no-go in terms of distributing.

kdmoyers

Got it!!!!!!!!!!    I worked it another way so I would not need the FromHandle function.

I just need the titles for my purposes.

However, it only works for the first chrome window it sees.
Next I have to make it iterate over all the chrome windows.

Code (winbatch) Select

; Load required assemblies.
ObjectClrOption("use","UIAutomationClient, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
ObjectClrOption("use","UIAutomationTypes, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
ObjectClrOption("useany","System")

; Search scope enumerated values.
enumScope  =  ObjectClrNew("System.Windows.Automation.TreeScope")

;  Get access to the desktop window which is the parent of all other windows.
objUiElement = ObjectClrNew("System.Windows.Automation.AutomationElement")
objUiRoot = objUiElement.RootElement

; Instantiate a property condition for an Chrome window.
objPropCon1 =  ObjectClrNew("System.Windows.Automation.PropertyCondition", objUiElement.ClassNameProperty, "Chrome_WidgetWin_1")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Find the first such window under the desktop

; set scope to "children"
nElement  = enumScope.Children
Scope = ObjectClrType("System.Windows.Automation.TreeScope",nElement)

; Find the first such under the desktop.
objChrome = objUiRoot.FindFirst(Scope,objPropCon1)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Find the all the tabitem type controls in chrome

; this is the "types" of controls
ConType = ObjectClrNew("System.Windows.Automation.ControlType")

; this is a "condition"
searchCon =  ObjectClrNew("System.Windows.Automation.PropertyCondition", objUiElement.ControlTypeProperty, ConType.TabItem);

; this is a "scope"
nElement  = enumScope.Descendants
Scope = ObjectClrType("System.Windows.Automation.TreeScope",nElement)

; Find that window.
objArr = objChrome.FindAll(Scope,searchCon)

lis = ""
ii = 0
foreach item in objArr
    x = item.GetCurrentPropertyValue(objUiElement.NameProperty)
    ii = ii + 1
    lis = iteminsert(ii:': ':x, -1, lis, @lf)
next

message("Chrome tabs",lis)

EXIT
The mind is everything; What you think, you become.

kdmoyers

This does ALL the chrome windows
Code (winbatch) Select
; Load required assemblies.
ObjectClrOption("use","UIAutomationClient, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
ObjectClrOption("use","UIAutomationTypes, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
ObjectClrOption("useany","System")

; Search scope enumerated values.
enumScope  =  ObjectClrNew("System.Windows.Automation.TreeScope")

;  Get access to the desktop window which is the parent of all other windows.
objUiElement = ObjectClrNew("System.Windows.Automation.AutomationElement")
objUiRoot = objUiElement.RootElement

; Instantiate a property condition for an Chrome window.
objPropCon1 =  ObjectClrNew("System.Windows.Automation.PropertyCondition", objUiElement.ClassNameProperty, "Chrome_WidgetWin_1")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Find the first such window under the desktop

; set scope to "children"
nElement  = enumScope.Children
Scope = ObjectClrType("System.Windows.Automation.TreeScope",nElement)

; Find all of those under the desktop.
objChromeArr = objUiRoot.FindAll(Scope,objPropCon1)

; for each one...
foreach objChrome in objChromeArr
   
    ; get the main window title
    maintitle = objChrome.GetCurrentPropertyValue(objUiElement.NameProperty)

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Find the all the tabitem type controls in chrome
   
    ; this is the "types" of controls
    ConType = ObjectClrNew("System.Windows.Automation.ControlType")
   
    ; this is a "condition"
    searchCon =  ObjectClrNew("System.Windows.Automation.PropertyCondition", objUiElement.ControlTypeProperty, ConType.TabItem);
   
    ; this is a "scope"
    nElement  = enumScope.Descendants
    Scope = ObjectClrType("System.Windows.Automation.TreeScope",nElement)
   
    ; Find that window.
    objArr = objChrome.FindAll(Scope,searchCon)
   
    lis = 'Main Window title="':maintitle:'"':@lf:@lf
    ii = 0
    foreach item in objArr
        x = item.GetCurrentPropertyValue(objUiElement.NameProperty)
        ii = ii + 1
        lis = iteminsert(ii:': ':x, -1, lis, @lf)
    next
   
    message("Chrome tabs",lis)

next

EXIT
The mind is everything; What you think, you become.

kdmoyers

It's interesting to note that the FindAll function actually takes noticeable time (1 or 2 seconds) to execute, with a bit of a CPU spike.
You are not going to want to put this inside a tight loop!

Also, I have only the barest glimmer of what this actually does.  I am the quintessential script kiddie, slapping together bits of code taken from the tech database, until I get something that works.  Just the worst.

-Kirby
The mind is everything; What you think, you become.

td

Nice bit of work. Cycling using keystrokes is something the user is going to notice. I should have noted that when I posted the example.

FWIW, the .Net automation classes are a cover for "old-fashioned" Windows COM-based UI Automation interfaces. They were added to Windows years ago to support "Windows Accessibility Features".  The APIs work with built-in Windows User controls and Shell controls that desktop applications often use. They work with other types of windows too but the application creating the windows may need to be coded with a small bit of UI Automation support for the APIs to work properly.
"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

stanl

Quote from: td on December 20, 2021, 02:10:49 PM
Nice bit of work.


Agree 100%.  I tested and on my Win 10 you can

ObjectClrOption("useany","UIAutomationClient")
ObjectClrOption("useany","UIAutomationTypes")


without failure.






td

Kirby's script is enshrined in The Tech Database. It was updated to use "useany" in place of "use" in the calls to ObjectClrOption before it was added. Just need to get Kirby's permission to add credit to the article.

https://techsupt.winbatch.com/webcgi/webbatch.exe?techsupt/nftechsupt.web+WinBatch/dotNet/System_Windows/System_Window_Automation+Use~System.Windows.Automation~to~List~Chrome~Tabs.txt
"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

kdmoyers

The mind is everything; What you think, you become.