This solution will detect a new instance of Chrome browser (new window) and if it is on your Teams' virtual desktop will move it to your other virtual desktop. The virtual desktop that is in focus does not change during this move although the solution could be tweaked to do so.
Tools Used
- VirtualDesktop
- MScholtes' Github repository VirtualDesktop contains a free open-source command line tool to manage virtual desktops in Windows 10 and Windows 11
- The tool includes many features, including the ability to identify which virtual desktop is visible and to move specific Windows from one virtual desktop to another.
- Installation:
- Download the tool (ZIP) from Github
- Unzip to a local folder
- In that folder run
Compile.bat
which will create several executables each for different versions of Windows.
- AutoHotKey
- AutoHotkey (AHK) is a free, open-source well-documented scripting language for Windows that allows users to easily create small to complex scripts for all kinds of tasks.
- For your use case, their
WinHook
command creates Windows Shell Hooks to monitor Windows events while using very few resources. This can be leveraged to detect whenever a new Chrome window is created.
- Installation: navigate to AutoHotkey and follow the instructions on the site.
- Also see Safe Browsing Note and Norton Safe Web Report vis-a-vis false positives about harmful programs.
Solution
NB: In this solution, the virtual desktop with the MS Teams application is arbitrarily named Teams
and the other virtual desktop is named Main
- AutoHotKey script
detect_chrome_window.ahk
watches for new windows launched by Chrome
- When a new instance of Chrome is detected, the script retrieves its handle/unique ID (HWND)
- The script then runs a Windows batch file
move_chrome_window.bat
while passing the handle as a parameter
- Batch script uses VirtualDesktop to check if the current virtual desktop is
Teams
- If it is, the script moves the new Chrome instance from
Teams
to Main
Detect_chrome_window.ahk
- This script was adapted from a script by FanaticGuru. They included ~200 lines of comments which are an incredible resource.
- You need only replace
C:\path\to\batch\move_chrome_window.bat
on 5th line with your own path and filename (if different)
WinHook.Shell.Add("NameOfFuncToRun",,, "chrome.exe",1) ; Chrome Window Created (1 at end means created)
NameOfFuncToRun(Win_Hwnd, Win_Title, Win_Class, Win_Exe, Win_Event)
{
run C:\path\to\batch\move_chrome_window.bat "%Win_Hwnd%",,Hide
}
;
;{============================
;
; Class (Nested): WinHook.Shell
;
; Method:
; Add(Func, wTitle:="", wClass:="", wExe:="", Event:=0)
;
; Desc: Add Shell Hook
;
; Parameters:
; 1) {Func} Function name or Function object to call on event
; 2) {wTitle} window Title to watch for event (default = "", all windows)
; 3) {wClass} window Class to watch for event (default = "", all windows)
; 4) {wExe} window Exe to watch for event (default = "", all windows)
; 5) {Event} Event (default = 0, all events)
;
; Returns: {Index} index to hook that can be used to Remove hook
;
; Shell Hook Events:
; 1 = HSHELL_WINDOWCREATED
; 2 = HSHELL_WINDOWDESTROYED
; 3 = HSHELL_ACTIVATESHELLWINDOW
; 4 = HSHELL_WINDOWACTIVATED
; 5 = HSHELL_GETMINRECT
; 6 = HSHELL_REDRAW
; 7 = HSHELL_TASKMAN
; 8 = HSHELL_LANGUAGE
; 9 = HSHELL_SYSMENU
; 10 = HSHELL_ENDTASK
; 11 = HSHELL_ACCESSIBILITYSTATE
; 12 = HSHELL_APPCOMMAND
; 13 = HSHELL_WINDOWREPLACED
; 14 = HSHELL_WINDOWREPLACING
; 32768 = 0x8000 = HSHELL_HIGHBIT
; 32772 = 0x8000 + 4 = 0x8004 = HSHELL_RUDEAPPACTIVATED (HSHELL_HIGHBIT + HSHELL_WINDOWACTIVATED)
; 32774 = 0x8000 + 6 = 0x8006 = HSHELL_FLASH (HSHELL_HIGHBIT + HSHELL_REDRAW)
;
; Note: ObjBindMethod(obj, Method) can be used to create a function object to a class method
; WinHook.Shell.Add(ObjBindMethod(TestClass.TestNestedClass, "MethodName"), wTitle, wClass, wExe, Event)
;
; ----------
;
; Desc: Function Called on Event
; FuncOrMethod(Win_Hwnd, Win_Title, Win_Class, Win_Exe, Win_Event)
;
; Parameters:
; 1) {Win_Hwnd} window handle ID of window with event
; 2) {Win_Title} window Title of window with event
; 3) {Win_Class} window Class of window with event
; 4) {Win_Exe} window Exe of window with event
; 5) {Win_Event} window Event
;
; Note: FuncOrMethod will be called with DetectHiddenWindows On.
;
; --------------------
;
; Method: Report(ByRef Object)
;
; Desc: Report Shell Hooks
;
; Returns: string report
; ByRef Object[Index].{Func, Title:, Class, Exe, Event}
;
; --------------------
;
; Method: Remove(Index)
; Method: Deregister()
;
;{============================
;
; Class (Nested): WinHook.Event
;
; Method:
; Add(eventMin, eventMax, eventProc, idProcess, WinTitle := "")
;
; Desc: Add Event Hook
;
; Parameters:
; 1) {eventMin} lowest Event value handled by the hook function
; 2) {eventMax} highest event value handled by the hook function
; 3) {eventProc} event hook function, call be function name or function object
; 4) {idProcess} ID of the process from which the hook function receives events (default = 0, all processes)
; 5) {WinTitle} WinTitle to identify which windows to operate on, (default = "", all windows)
;
; Returns: {hWinEventHook} handle to hook that can be used to unhook
;
; Event Hook Events:
; 0x8012 = EVENT_OBJECT_ACCELERATORCHANGE
; 0x8017 = EVENT_OBJECT_CLOAKED
; 0x8015 = EVENT_OBJECT_CONTENTSCROLLED
; 0x8000 = EVENT_OBJECT_CREATE
; 0x8011 = EVENT_OBJECT_DEFACTIONCHANGE
; 0x800D = EVENT_OBJECT_DESCRIPTIONCHANGE
; 0x8001 = EVENT_OBJECT_DESTROY
; 0x8021 = EVENT_OBJECT_DRAGSTART
; 0x8022 = EVENT_OBJECT_DRAGCANCEL
; 0x8023 = EVENT_OBJECT_DRAGCOMPLETE
; 0x8024 = EVENT_OBJECT_DRAGENTER
; 0x8025 = EVENT_OBJECT_DRAGLEAVE
; 0x8026 = EVENT_OBJECT_DRAGDROPPED
; 0x80FF = EVENT_OBJECT_END
; 0x8005 = EVENT_OBJECT_FOCUS
; 0x8010 = EVENT_OBJECT_HELPCHANGE
; 0x8003 = EVENT_OBJECT_HIDE
; 0x8020 = EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED
; 0x8028 = EVENT_OBJECT_IME_HIDE
; 0x8027 = EVENT_OBJECT_IME_SHOW
; 0x8029 = EVENT_OBJECT_IME_CHANGE
; 0x8013 = EVENT_OBJECT_INVOKED
; 0x8019 = EVENT_OBJECT_LIVEREGIONCHANGED
; 0x800B = EVENT_OBJECT_LOCATIONCHANGE
; 0x800C = EVENT_OBJECT_NAMECHANGE
; 0x800F = EVENT_OBJECT_PARENTCHANGE
; 0x8004 = EVENT_OBJECT_REORDER
; 0x8006 = EVENT_OBJECT_SELECTION
; 0x8007 = EVENT_OBJECT_SELECTIONADD
; 0x8008 = EVENT_OBJECT_SELECTIONREMOVE
; 0x8009 = EVENT_OBJECT_SELECTIONWITHIN
; 0x8002 = EVENT_OBJECT_SHOW
; 0x800A = EVENT_OBJECT_STATECHANGE
; 0x8030 = EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED
; 0x8014 = EVENT_OBJECT_TEXTSELECTIONCHANGED
; 0x8018 = EVENT_OBJECT_UNCLOAKED
; 0x800E = EVENT_OBJECT_VALUECHANGE
; 0x0002 = EVENT_SYSTEM_ALERT
; 0x8016 = EVENT_SYSTEM_ARRANGMENTPREVIEW
; 0x0009 = EVENT_SYSTEM_CAPTUREEND
; 0x0008 = EVENT_SYSTEM_CAPTURESTART
; 0x000D = EVENT_SYSTEM_CONTEXTHELPEND
; 0x000C = EVENT_SYSTEM_CONTEXTHELPSTART
; 0x0020 = EVENT_SYSTEM_DESKTOPSWITCH
; 0x0011 = EVENT_SYSTEM_DIALOGEND
; 0x0010 = EVENT_SYSTEM_DIALOGSTART
; 0x000F = EVENT_SYSTEM_DRAGDROPEND
; 0x000E = EVENT_SYSTEM_DRAGDROPSTART
; 0x00FF = EVENT_SYSTEM_END
; 0x0003 = EVENT_SYSTEM_FOREGROUND
; 0x0007 = EVENT_SYSTEM_MENUPOPUPEND
; 0x0006 = EVENT_SYSTEM_MENUPOPUPSTART
; 0x0005 = EVENT_SYSTEM_MENUEND
; 0x0004 = EVENT_SYSTEM_MENUSTART
; 0x0017 = EVENT_SYSTEM_MINIMIZEEND
; 0x0016 = EVENT_SYSTEM_MINIMIZESTART
; 0x000B = EVENT_SYSTEM_MOVESIZEEND
; 0x000A = EVENT_SYSTEM_MOVESIZESTART
; 0x0013 = EVENT_SYSTEM_SCROLLINGEND
; 0x0012 = EVENT_SYSTEM_SCROLLINGSTART
; 0x0001 = EVENT_SYSTEM_SOUND
; 0x0015 = EVENT_SYSTEM_SWITCHEND
; 0x0014 = EVENT_SYSTEM_SWITCHSTART
;
; Note: ObjBindMethod(obj, Method) can be used to create a function object to a class method
; WinHook.Event.Add((eventMin, eventMax, ObjBindMethod(TestClass.TestNestedClass, "MethodName"), idProcess, WinTitle := "")
;
; ----------
;
; Desc: Function Called on Event
; FuncOrMethod(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime)
;
; Parameters:
; 1) {hWinEventHook} Handle to an event hook instance.
; 2) {event} Event that occurred. This value is one of the event constants
; 3) {hwnd} Handle to the window that generates the event.
; 4) {idObject} Identifies the object that is associated with the event.
; 5) {idChild} Child ID if the event was triggered by a child element.
; 6) {dwEventThread} Identifies the thread that generated the event.
; 7) {dwmsEventTime} Specifies the time, in milliseconds, that the event was generated.
;
; Note: FuncOrMethod will be called with DetectHiddenWindows On.
;
; --------------------
;
; Method: Report(ByRef Object)
;
; Returns: string report
; ByRef Object[hWinEventHook].{eventMin, eventMax, eventProc, idProcess, WinTitle}
;
; --------------------
;
; Method: UnHook(hWinEventHook)
; Method: UnHookAll()
;
;{============================
class WinHook
{
class Shell
{
Add(Func, wTitle:="", wClass:="", wExe:="", Event:=0)
{
if !WinHook.Shell.Hooks
{
WinHook.Shell.Hooks := {}, WinHook.Shell.Events := {}
DllCall("RegisterShellHookWindow", UInt, A_ScriptHwnd)
MsgNum := DllCall("RegisterWindowMessage", Str, "SHELLHOOK")
OnMessage(MsgNum, ObjBindMethod(WinHook.Shell, "Message"))
}
if !IsObject(Func)
Func := Func(Func)
WinHook.Shell.Hooks.Push({Func: Func, Title: wTitle, Class: wClass, Exe: wExe, Event: Event})
WinHook.Shell.Events[Event] := true
return WinHook.Shell.Hooks.MaxIndex()
}
Remove(Index)
{
WinHook.Shell.Hooks.Delete(Index)
WinHook.Shell.Events[Event] := {} ; delete and rebuild Event list
For key, Hook in WinHook.Shell.Hooks
WinHook.Shell.Events[Hook.Event] := true
}
Report(ByRef Obj:="")
{
Obj := WinHook.Shell.Hooks
For key, Hook in WinHook.Shell.Hooks
Display .= key "|" Hook.Event "|" Hook.Func.Name "|" Hook.Title "|" Hook.Class "|" Hook.Exe "`n"
return Trim(Display, "`n")
}
Deregister()
{
DllCall("DeregisterShellHookWindow", UInt, A_ScriptHwnd)
WinHook.Shell.Hooks := "", WinHook.Shell.Events := ""
}
Message(Event, Hwnd) ; Private Method
{
DetectHiddenWindows, On
If (WinHook.Shell.Events[Event] or WinHook.Shell.Events[0])
{
WinGetTitle, wTitle, ahk_id %Hwnd%
WinGetClass, wClass, ahk_id %Hwnd%
WinGet, wExe, ProcessName, ahk_id %Hwnd%
for key, Hook in WinHook.Shell.Hooks
if ((Hook.Title = wTitle or Hook.Title = "") and (Hook.Class = wClass or Hook.Class = "") and (Hook.Exe = wExe or Hook.Exe = "") and (Hook.Event = Event or Hook.Event = 0))
return Hook.Func.Call(Hwnd, wTitle, wClass, wExe, Event)
}
}
}
class Event
{
Add(eventMin, eventMax, eventProc, idProcess := 0, WinTitle := "")
{
if !WinHook.Event.Hooks
{
WinHook.Event.Hooks := {}
static CB_WinEventProc := RegisterCallback(WinHook.Event.Message)
OnExit(ObjBindMethod(WinHook.Event, "UnHookAll"))
}
hWinEventHook := DllCall("SetWinEventHook"
, "UInt", eventMin ; UINT eventMin
, "UInt", eventMax ; UINT eventMax
, "Ptr" , 0x0 ; HMODULE hmodWinEventProc
, "Ptr" , CB_WinEventProc ; WINEVENTPROC lpfnWinEventProc
, "UInt" , idProcess ; DWORD idProcess
, "UInt", 0x0 ; DWORD idThread
, "UInt", 0x0|0x2) ; UINT dwflags, OutOfContext|SkipOwnProcess
if !IsObject(eventProc)
eventProc := Func(eventProc)
WinHook.Event.Hooks[hWinEventHook] := {eventMin: eventMin, eventMax: eventMax, eventProc: eventProc, idProcess: idProcess, WinTitle: WinTitle}
return hWinEventHook
}
Report(ByRef Obj:="")
{
Obj := WinHook.Event.Hooks
For hWinEventHook, Hook in WinHook.Event.Hooks
Display .= hWinEventHook "|" Hook.eventMin "|" Hook.eventMax "|" Hook.eventProc.Name "|" Hook.idProcess "|" Hook.WinTitle "`n"
return Trim(Display, "`n")
}
UnHook(hWinEventHook)
{
DllCall("UnhookWinEvent", "Ptr", hWinEventHook)
WinHook.Event.Hooks.Delete(hWinEventHook)
}
UnHookAll()
{
for hWinEventHook, Hook in WinHook.Event.Hooks
DllCall("UnhookWinEvent", "Ptr", hWinEventHook)
WinHook.Event.Hooks := "", CB_WinEventProc := ""
}
Message(event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime) ; 'Private Method
{
DetectHiddenWindows, On
Hook := WinHook.Event.Hooks[hWinEventHook := this] ; this' is hidden param1 because method is called as func
WinGet, List, List, % Hook.WinTitle
Loop % List
if (List%A_Index% = hwnd)
return Hook.eventProc.Call(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime)
}
}
}
move_chrome_window.bat
- VirtualDesktop will compile multiple versions of the executable because of breaking changes between Windows 10, Windows 11 as well as builds of each. I am using Windows 11, so my executable for VirtualDesktop is
VirtualDesktop11.exe
yours will likely be VirtualDesktop.exe
. If it is something different the batch file should be updated to reflect that.
- I added the path to
VirtualDesktop.exe
to my PATH environment variable so I don't need to specify the path in the batch file. If you don't do that you need to add the path in the batch file
- If you use other names than
Main
and Teams
for the virtual desktops, you will need to modify the batch file to reflect that.
@echo off
setlocal enabledelayedexpansion
rem Check if 'Teams' is visible
FOR /F "tokens=*" %%F IN ('VirtualDesktop /IsVisible:Teams') DO (
set myvar=%%F
if not "!myvar:is visible=!"=="!myvar!" (
rem move Chrome window from 'Teams' to 'Main'
VirtualDesktop "/GetDesktop:main" "/MoveWindowHandle:%1"
)
)
--new-window
generally because if Chrome is in the right virtual desktop then it does indeed open in a new tab in the existing window.