001package com.randomnoun.common.jna; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006 007import java.util.HashMap; 008import java.util.Map; 009 010import javax.xml.parsers.DocumentBuilder; 011import javax.xml.parsers.DocumentBuilderFactory; 012import javax.xml.parsers.ParserConfigurationException; 013import javax.xml.transform.TransformerException; 014 015import org.apache.log4j.Logger; 016import org.w3c.dom.Document; 017import org.w3c.dom.Element; 018 019import com.randomnoun.common.XmlUtil; 020import com.sun.jna.Native; 021import com.sun.jna.Pointer; 022import com.sun.jna.platform.win32.WinDef.DWORD; 023import com.sun.jna.platform.win32.WinDef.HWND; 024import com.sun.jna.platform.win32.WinUser; 025import com.sun.jna.platform.win32.WinUser.WNDENUMPROC; 026import com.sun.jna.win32.StdCallLibrary; 027import com.sun.jna.win32.W32APIOptions; 028 029/** A class to convert the Win32 windows tree into a DOM object 030 * 031 * @see <a href="http://www.randomnoun.com/wp/2012/12/26/automating-windows-from-java-and-windowtreedom/">http://www.randomnoun.com/wp/2012/12/26/automating-windows-from-java-and-windowtreedom/</a> 032 * @author knoxg 033 */ 034public class WindowTreeDom { 035 036 // the User32 functions we invoke from this class 037 public interface User32 extends StdCallLibrary { 038 User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class, 039 W32APIOptions.DEFAULT_OPTIONS); 040 041 public static final DWORD GW_OWNER = new DWORD(4); 042 boolean EnumWindows(WinUser.WNDENUMPROC lpEnumFunc, Pointer arg); 043 boolean EnumChildWindows(HWND hWnd, WNDENUMPROC lpEnumFunc, Pointer data); 044 int GetWindowText(HWND hWnd, char[] lpString, int nMaxCount); 045 int GetClassName(HWND hWnd, char[] lpClassName, int nMaxCount); 046 public HWND GetWindow(HWND hWnd, DWORD cmd); 047 HWND GetParent(HWND hWnd); 048 } 049 050 /** JNA interface to USER32.DLL */ 051 final static User32 lib = User32.INSTANCE; 052 053 /** Logger instance for this class */ 054 static Logger logger = Logger.getLogger(WindowTreeDom.class); 055 056 /** WindowTreeDom constructor. 057 * 058 * @see #getDom() 059 */ 060 public WindowTreeDom() { 061 062 } 063 064 /** This callback is invoked for each window found. It generates XML 065 * {#link org.w3c.Element}s for each window, and attaches them to the supplied 066 * {#link org.w3c.Document}. 067 * 068 */ 069 private static class WindowCallback implements WinUser.WNDENUMPROC { 070 Document doc; 071 Element documentElement; 072 Element topLevelWindow; 073 Map<String, Element> hwndMap = new HashMap<String, Element>(); 074 075 /** Creates a new window callback 076 * 077 * @param doc The XML document populated by this callback. 078 * @param topLevelHWND If non-null, the windows being returned should all be 079 * child windows of this HWND (via EnumChildWindows), otherwise it is 080 * assumed toplevel windows are returned (via EnumWindows) 081 * @param topLevelWindow If non-null, the document Element within <tt>doc</tt> 082 * which will contain new child elements. 083 */ 084 public WindowCallback(Document doc, HWND topLevelHWND, Element topLevelWindow) { 085 this.doc = doc; 086 this.topLevelWindow = topLevelWindow; 087 if (topLevelWindow != null) { 088 hwndMap.put(topLevelHWND.getPointer().toString(), topLevelWindow); 089 } 090 documentElement = doc.getDocumentElement(); 091 } 092 093 public boolean callback(HWND hWnd, Pointer data) { 094 095 char[] buffer = new char[512]; 096 User32.INSTANCE.GetWindowText(hWnd, buffer, 512); 097 098 char[] buffer2 = new char[1026]; 099 int classLen = User32.INSTANCE.GetClassName(hWnd, buffer2, 1026); 100 101 String windowTitle = Native.toString(buffer); 102 String className = Native.toString(buffer2); 103 104 HWND parent = User32.INSTANCE.GetParent(hWnd); 105 HWND owner = User32.INSTANCE.GetWindow(hWnd, User32.GW_OWNER); 106 107 // check if this has already been created in the DOM 108 Element el = hwndMap.get(hWnd.getPointer().toString()); 109 if (el==null) { 110 el = doc.createElement("window"); 111 } else { 112 el.removeAttribute("pwindow"); 113 } 114 el.setAttribute("hwnd", hWnd.getPointer().toString()); 115 if (owner!=null) { 116 el.setAttribute("owner", owner.getPointer().toString()); 117 } 118 el.setAttribute("title", windowTitle); 119 el.setAttribute("class", className); 120 121 hwndMap.put(hWnd.getPointer().toString(), el); 122 if (topLevelWindow==null) { 123 // this is a real top level element, so enumerate its children 124 WindowCallback childDommer = new WindowCallback(doc, hWnd, el); 125 // this code relies on being able to enum child windows whilst enumming toplevel windows 126 lib.EnumChildWindows (hWnd, childDommer, new Pointer(0)); 127 try { 128 childDommer.checkForOrphanedWindows(); 129 } catch (TransformerException e) { 130 logger.error("Problem serialising orphaned windows to XML", e); 131 } 132 } 133 134 if (parent==null) { 135 documentElement.appendChild(el); 136 if (topLevelWindow!=null) { 137 // have seen VMDragDetectWndClass'es here, presumably a vmware thing 138 // (note that this window won't be in the parent callback's hwndMap) 139 try { 140 logger.warn("Toplevel child window found: " + XmlUtil.getXmlString(el, true)); 141 } catch (TransformerException e) { 142 logger.error("Toplevel child window found, problem serialising toplevel windows to XML", e); 143 } 144 } 145 146 } else { 147 Element parentEl = hwndMap.get(parent.getPointer().toString()); 148 if (parentEl==null) { 149 // throw new IllegalStateException("Unknown parent window '" + parent.getPointer().toString() + "'"); 150 // it appears that we can get IME child windows being returned 151 // by EnumWindows, even though they're not top-level 152 parentEl = doc.createElement("window"); 153 parentEl.setAttribute("pwindow", "true"); 154 parentEl.setAttribute("hwnd", parent.getPointer().toString()); 155 hwndMap.put(parent.getPointer().toString(), parentEl); 156 } 157 parentEl.appendChild(el); 158 } 159 160 return true; 161 } 162 163 /** Lists any window nodes that were generated via enumeration, whose 164 * parent nodes were not generated. 165 * 166 * @throws TransformerException 167 */ 168 public void checkForOrphanedWindows() throws TransformerException { 169 for (Element e : hwndMap.values()) { 170 if (!e.getAttribute("pwindow").equals("")) { 171 // the desktop window isn't in the enumeration 172 logger.warn("Parent window found that was not in enumeration: " + XmlUtil.getXmlString(e, true)); 173 // throw new IllegalStateException("Window found without parent window"); 174 } 175 } 176 } 177 178 } 179 180 /** Generate an XML document from the Win32 window tree */ 181 public Document getDom() throws ParserConfigurationException, TransformerException { 182 DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); 183 DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); 184 Document doc = docBuilder.newDocument(); 185 Element topElement = doc.createElement("windows"); 186 doc.appendChild(topElement); 187 188 WindowCallback dommer = new WindowCallback(doc, null, null); 189 lib.EnumWindows (dommer, new Pointer(0)); 190 dommer.checkForOrphanedWindows(); 191 192 return doc; 193 } 194 195 /** Return the hwnd of an element, as a pointer represented as a long 196 * 197 * @param windowEl a window element returned from getDom() 198 * 199 * @return the hwnd of the element. 200 */ 201 public HWND getHwnd(Element windowEl) { 202 String hwndString = windowEl.getAttribute("hwnd"); 203 if (hwndString.startsWith("native@0x")) { 204 return new HWND(new Pointer(Long.parseLong(hwndString.substring(9), 16))); 205 } else { 206 throw new IllegalStateException("Could not determine HWND of window element: found '" + hwndString + "'"); 207 } 208 } 209 210}