I have a python script called from a VBA AutoNew() sub in a MS Word 2003 template (.dot) - so it runs every time a document is created from this Word template.
A third-party application creates documents from this template. There are a number of formatting issues with how the third-party application sets up the document, thus my script to tweak it once the third-party script has finished running. (I originally wrote the script in VBA, but problems with the VBA timer cause it to crash a significant fraction of the time. The python version works flawlessly.)
I want the script to work with just the document that called it, which will always be the most recently opened Word file. (The file is a .doc and not a .docx, if that makes any difference.) I have found three ways to get an open instance of Word (since this script is called by AutoNew there will always be an available open instance):
win32com.client.GetActiveObject (Class = 'Word.Application')
win32com.client.gencache.EnsureDispatch('Word.Application')
win32com.client.Dispatch('Word.Application')
Any of these three work great if the newly created document is the only open Word file. But if a Word document is already open, and I run the third-party software to create a new document from this template, the python script grabs the older instance every time with all three methods.
I've tried searching for ways to loop through Word documents, with the idea that I could check all the names and select the one with the highest number at the end (at the time the script runs the document will not be saved, so its name will be Document1, Document2, etc.) Unfortunately I only found methods to loop through closed documents (opening one, doing something, closing it, moving on to the next), not (as in my case) already open ones.
Is there a way to direct python to the most recently opened Word document?
EDIT Related question: Word VBA and Multiple Word Instances
I have found how to get the Windows handle integer of the document I want to control:
import win32gui
import re
#Create a list of all open Microsoft Word document titles and their
#handle integers
titles = []
def foreach_window(hwnd, lParam):
if win32gui.IsWindowVisible(hwnd):
title = win32gui.GetWindowText(hwnd)
if 'Microsoft Word' in title:
titles.append([title, hwnd])
return True
win32gui.EnumWindows(foreach_window, None)
#Find the handle of the newest open, unsaved Word document
winOrder = []
for s in titles:
item = re.search(r'Document\d', s[0])
if item:
winOrder.append(int(re.search(r'\d+', s[0]).group()))
else:
winOrder.append(0)
hwnd = titles[winOrder.index(max(winOrder))][1]
#Get the edit window from inside the Word instance
def callback(hwnd, hwnds):
if win32gui.GetClassName(hwnd) == '_WwG':
hwnds.append(hwnd)
#I think there should be a 'return False' here to let EnumChildWindows
#know it doesn't have to keep looping once it finds the edit window,
#but it crashes with 'pywintypes.error: (0, 'EnumChildWindows',
#'No error message is available') if I try that
return True
hwnds = []
win32gui.EnumChildWindows(whndl, callback, hwnds)
#Something like this...
#window = win32gui.AccessibleObjectFromWindow(hwnds[0])
So now - how to create a COM object from the Windows handle?
Some searching through the GitHub code of NVDA (Non-Visual Desktop Access) finally got me the object I was looking for:
#Part of the pywin32 package that must be installed with the pywin32
#installer:
import win32com.client as win32
import win32gui
from ctypes import oledll
from ctypes import byref
#installed by easy_install comtypes
from comtypes import POINTER
from comtypes.automation import IDispatch
import comtypes.client.dynamic as comDy
#Handle integer hwnds[0] per my edit in the question
OBJID_NATIVEOM = -16
p = POINTER(IDispatch)()
oledll.oleacc.AccessibleObjectFromWindow(hwnds[0], OBJID_NATIVEOM,
byref(IDispatch._iid_), byref(p))
window = comDy.Dispatch(p)
word = window.application
cert = word.Documents(1)