#!/usr/bin/env python # Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """SiteCompare module for invoking, locating, and manipulating windows. This module is a catch-all wrapper for operating system UI functionality that doesn't belong in other modules. It contains functions for finding particular windows, scraping their contents, and invoking processes to create them. """ import os import string import time import PIL.ImageGrab import pywintypes import win32event import win32gui import win32process def FindChildWindows(hwnd, path): """Find a set of windows through a path specification. Args: hwnd: Handle of the parent window path: Path to the window to find. Has the following form: "foo/bar/baz|foobar/|foobarbaz" The slashes specify the "path" to the child window. The text is the window class, a pipe (if present) is a title. * is a wildcard and will find all child windows at that level Returns: A list of the windows that were found """ windows_to_check = [hwnd] # The strategy will be to take windows_to_check and use it # to find a list of windows that match the next specification # in the path, then repeat with the list of found windows as the # new list of windows to check for segment in path.split("/"): windows_found = [] check_values = segment.split("|") # check_values is now a list with the first element being # the window class, the second being the window caption. # If the class is absent (or wildcarded) set it to None if check_values[0] == "*" or not check_values[0]: check_values[0] = None # If the window caption is also absent, force it to None as well if len(check_values) == 1: check_values.append(None) # Loop through the list of windows to check for window_check in windows_to_check: window_found = None while window_found != 0: # lint complains, but 0 != None if window_found is None: window_found = 0 try: # Look for the next sibling (or first sibling if window_found is 0) # of window_check with the specified caption and/or class window_found = win32gui.FindWindowEx( window_check, window_found, check_values[0], check_values[1]) except pywintypes.error, e: # FindWindowEx() raises error 2 if not found if e[0] == 2: window_found = 0 else: raise e # If FindWindowEx struck gold, add to our list of windows found if window_found: windows_found.append(window_found) # The windows we found become the windows to check for the next segment windows_to_check = windows_found return windows_found def FindChildWindow(hwnd, path): """Find a window through a path specification. This method is a simple wrapper for FindChildWindows() for the case (the majority case) where you expect to find a single window Args: hwnd: Handle of the parent window path: Path to the window to find. See FindChildWindows() Returns: The window that was found """ return FindChildWindows(hwnd, path)[0] def ScrapeWindow(hwnd, rect=None): """Scrape a visible window and return its contents as a bitmap. Args: hwnd: handle of the window to scrape rect: rectangle to scrape in client coords, defaults to the whole thing If specified, it's a 4-tuple of (left, top, right, bottom) Returns: An Image containing the scraped data """ # Activate the window SetForegroundWindow(hwnd) # If no rectangle was specified, use the fill client rectangle if not rect: rect = win32gui.GetClientRect(hwnd) upper_left = win32gui.ClientToScreen(hwnd, (rect[0], rect[1])) lower_right = win32gui.ClientToScreen(hwnd, (rect[2], rect[3])) rect = upper_left+lower_right return PIL.ImageGrab.grab(rect) def SetForegroundWindow(hwnd): """Bring a window to the foreground.""" win32gui.SetForegroundWindow(hwnd) def InvokeAndWait(path, cmdline="", timeout=10, tick=1.): """Invoke an application and wait for it to bring up a window. Args: path: full path to the executable to invoke cmdline: command line to pass to executable timeout: how long (in seconds) to wait before giving up tick: length of time to wait between checks Returns: A tuple of handles to the process and the application's window, or (None, None) if it timed out waiting for the process """ def EnumWindowProc(hwnd, ret): """Internal enumeration func, checks for visibility and proper PID.""" if win32gui.IsWindowVisible(hwnd): # don't bother even checking hidden wnds pid = win32process.GetWindowThreadProcessId(hwnd)[1] if pid == ret[0]: ret[1] = hwnd return 0 # 0 means stop enumeration return 1 # 1 means continue enumeration # We don't need to change anything about the startupinfo structure # (the default is quite sufficient) but we need to create it just the # same. sinfo = win32process.STARTUPINFO() proc = win32process.CreateProcess( path, # path to new process's executable cmdline, # application's command line None, # process security attributes (default) None, # thread security attributes (default) False, # inherit parent's handles 0, # creation flags None, # environment variables None, # directory sinfo) # default startup info # Create process returns (prochandle, pid, threadhandle, tid). At # some point we may care about the other members, but for now, all # we're after is the pid pid = proc[2] # Enumeration APIs can take an arbitrary integer, usually a pointer, # to be passed to the enumeration function. We'll pass a pointer to # a structure containing the PID we're looking for, and an empty out # parameter to hold the found window ID ret = [pid, None] tries_until_timeout = timeout/tick num_tries = 0 # Enumerate top-level windows, look for one with our PID while num_tries < tries_until_timeout and ret[1] is None: try: win32gui.EnumWindows(EnumWindowProc, ret) except pywintypes.error, e: # error 0 isn't an error, it just meant the enumeration was # terminated early if e[0]: raise e time.sleep(tick) num_tries += 1 # TODO(jhaas): Should we throw an exception if we timeout? Or is returning # a window ID of None sufficient? return (proc[0], ret[1]) def WaitForProcessExit(proc, timeout=None): """Waits for a given process to terminate. Args: proc: handle to process timeout: timeout (in seconds). None = wait indefinitely Returns: True if process ended, False if timed out """ if timeout is None: timeout = win32event.INFINITE else: # convert sec to msec timeout *= 1000 return (win32event.WaitForSingleObject(proc, timeout) == win32event.WAIT_OBJECT_0) def WaitForThrobber(hwnd, rect=None, timeout=20, tick=0.1, done=10): """Wait for a browser's "throbber" (loading animation) to complete. Args: hwnd: window containing the throbber rect: rectangle of the throbber, in client coords. If None, whole window timeout: if the throbber is still throbbing after this long, give up tick: how often to check the throbber done: how long the throbber must be unmoving to be considered done Returns: Number of seconds waited, -1 if timed out """ if not rect: rect = win32gui.GetClientRect(hwnd) # last_throbber will hold the results of the preceding scrape; # we'll compare it against the current scrape to see if we're throbbing last_throbber = ScrapeWindow(hwnd, rect) start_clock = time.clock() timeout_clock = start_clock + timeout last_changed_clock = start_clock; while time.clock() < timeout_clock: time.sleep(tick) current_throbber = ScrapeWindow(hwnd, rect) if current_throbber.tostring() != last_throbber.tostring(): last_throbber = current_throbber last_changed_clock = time.clock() else: if time.clock() - last_changed_clock > done: return last_changed_clock - start_clock return -1 def MoveAndSizeWindow(wnd, position=None, size=None, child=None): """Moves and/or resizes a window. Repositions and resizes a window. If a child window is provided, the parent window is resized so the child window has the given size Args: wnd: handle of the frame window position: new location for the frame window size: new size for the frame window (or the child window) child: handle of the child window Returns: None """ rect = win32gui.GetWindowRect(wnd) if position is None: position = (rect[0], rect[1]) if size is None: size = (rect[2]-rect[0], rect[3]-rect[1]) elif child is not None: child_rect = win32gui.GetWindowRect(child) slop = (rect[2]-rect[0]-child_rect[2]+child_rect[0], rect[3]-rect[1]-child_rect[3]+child_rect[1]) size = (size[0]+slop[0], size[1]+slop[1]) win32gui.MoveWindow(wnd, # window to move position[0], # new x coord position[1], # new y coord size[0], # new width size[1], # new height True) # repaint? def EndProcess(proc, code=0): """Ends a process. Wraps the OS TerminateProcess call for platform-independence Args: proc: process ID code: process exit code Returns: None """ win32process.TerminateProcess(proc, code) def URLtoFilename(url, path=None, extension=None): """Converts a URL to a filename, given a path. This in theory could cause collisions if two URLs differ only in unprintable characters (eg. http://www.foo.com/?bar and http://www.foo.com/:bar. In practice this shouldn't be a problem. Args: url: The URL to convert path: path to the directory to store the file extension: string to append to filename Returns: filename """ trans = string.maketrans(r'\/:*?"<>|', '_________') if path is None: path = "" if extension is None: extension = "" if len(path) > 0 and path[-1] != '\\': path += '\\' url = url.translate(trans) return "%s%s%s" % (path, url, extension) def PreparePath(path): """Ensures that a given path exists, making subdirectories if necessary. Args: path: fully-qualified path of directory to ensure exists Returns: None """ try: os.makedirs(path) except OSError, e: if e[0] != 17: raise e # error 17: path already exists def main(): PreparePath(r"c:\sitecompare\scrapes\ie7") # We're being invoked rather than imported. Let's do some tests # Hardcode IE's location for the purpose of this test (proc, wnd) = InvokeAndWait( r"c:\program files\internet explorer\iexplore.exe") # Find the browser pane in the IE window browser = FindChildWindow( wnd, "TabWindowClass/Shell DocObject View/Internet Explorer_Server") # Move and size the window MoveAndSizeWindow(wnd, (0, 0), (1024, 768), browser) # Take a screenshot i = ScrapeWindow(browser) i.show() EndProcess(proc, 0) if __name__ == "__main__": sys.exit(main())