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
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);
}
}
}
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.
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!
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.
Yep.
Jim
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
Another example with multiple authors from the Tech Database:
https://techsupt.winbatch.com/webcgi/webbatch.exe?techsupt/nftechsupt.web+WinBatch/dotNet/System_CodeDom+Grab~URL~from~Chrome.txt (https://techsupt.winbatch.com/webcgi/webbatch.exe?techsupt/nftechsupt.web+WinBatch/dotNet/System_CodeDom+Grab~URL~from~Chrome.txt)
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 (https://developer.chrome.com/docs/extensions/reference/processes/) 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 ;)
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.
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
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
Much to think about! Thanks everyone.
-Kirby
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.
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.
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:
; 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
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;
}
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.
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;
}
}
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.
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.
; 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
This does ALL the chrome windows; 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
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
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.
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.
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 (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)
Woo! sounds great!