Home | History | Annotate | Download | only in win32
      1 #!/usr/bin/env python
      2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """SiteCompare module for invoking, locating, and manipulating windows.
      7 
      8 This module is a catch-all wrapper for operating system UI functionality
      9 that doesn't belong in other modules. It contains functions for finding
     10 particular windows, scraping their contents, and invoking processes to
     11 create them.
     12 """
     13 
     14 import os
     15 import string
     16 import time
     17 
     18 import PIL.ImageGrab
     19 import pywintypes
     20 import win32event
     21 import win32gui
     22 import win32process
     23 
     24 
     25 def FindChildWindows(hwnd, path):
     26   """Find a set of windows through a path specification.
     27 
     28   Args:
     29     hwnd: Handle of the parent window
     30     path: Path to the window to find. Has the following form:
     31       "foo/bar/baz|foobar/|foobarbaz"
     32       The slashes specify the "path" to the child window.
     33       The text is the window class, a pipe (if present) is a title.
     34       * is a wildcard and will find all child windows at that level
     35 
     36   Returns:
     37     A list of the windows that were found
     38   """
     39   windows_to_check = [hwnd]
     40 
     41   # The strategy will be to take windows_to_check and use it
     42   # to find a list of windows that match the next specification
     43   # in the path, then repeat with the list of found windows as the
     44   # new list of windows to check
     45   for segment in path.split("/"):
     46     windows_found = []
     47     check_values = segment.split("|")
     48 
     49     # check_values is now a list with the first element being
     50     # the window class, the second being the window caption.
     51     # If the class is absent (or wildcarded) set it to None
     52     if check_values[0] == "*" or not check_values[0]: check_values[0] = None
     53 
     54     # If the window caption is also absent, force it to None as well
     55     if len(check_values) == 1: check_values.append(None)
     56 
     57     # Loop through the list of windows to check
     58     for window_check in windows_to_check:
     59       window_found = None
     60       while window_found != 0:  # lint complains, but 0 != None
     61         if window_found is None: window_found = 0
     62         try:
     63           # Look for the next sibling (or first sibling if window_found is 0)
     64           # of window_check with the specified caption and/or class
     65           window_found = win32gui.FindWindowEx(
     66             window_check, window_found, check_values[0], check_values[1])
     67         except pywintypes.error, e:
     68           # FindWindowEx() raises error 2 if not found
     69           if e[0] == 2:
     70             window_found = 0
     71           else:
     72             raise e
     73 
     74         # If FindWindowEx struck gold, add to our list of windows found
     75         if window_found: windows_found.append(window_found)
     76 
     77     # The windows we found become the windows to check for the next segment
     78     windows_to_check = windows_found
     79 
     80   return windows_found
     81 
     82 
     83 def FindChildWindow(hwnd, path):
     84   """Find a window through a path specification.
     85 
     86   This method is a simple wrapper for FindChildWindows() for the
     87   case (the majority case) where you expect to find a single window
     88 
     89   Args:
     90     hwnd: Handle of the parent window
     91     path: Path to the window to find. See FindChildWindows()
     92 
     93   Returns:
     94     The window that was found
     95   """
     96   return FindChildWindows(hwnd, path)[0]
     97 
     98 
     99 def ScrapeWindow(hwnd, rect=None):
    100   """Scrape a visible window and return its contents as a bitmap.
    101 
    102   Args:
    103     hwnd: handle of the window to scrape
    104     rect: rectangle to scrape in client coords, defaults to the whole thing
    105           If specified, it's a 4-tuple of (left, top, right, bottom)
    106 
    107   Returns:
    108     An Image containing the scraped data
    109   """
    110   # Activate the window
    111   SetForegroundWindow(hwnd)
    112 
    113   # If no rectangle was specified, use the fill client rectangle
    114   if not rect: rect = win32gui.GetClientRect(hwnd)
    115 
    116   upper_left  = win32gui.ClientToScreen(hwnd, (rect[0], rect[1]))
    117   lower_right = win32gui.ClientToScreen(hwnd, (rect[2], rect[3]))
    118   rect = upper_left+lower_right
    119 
    120   return PIL.ImageGrab.grab(rect)
    121 
    122 
    123 def SetForegroundWindow(hwnd):
    124   """Bring a window to the foreground."""
    125   win32gui.SetForegroundWindow(hwnd)
    126 
    127 
    128 def InvokeAndWait(path, cmdline="", timeout=10, tick=1.):
    129   """Invoke an application and wait for it to bring up a window.
    130 
    131   Args:
    132     path: full path to the executable to invoke
    133     cmdline: command line to pass to executable
    134     timeout: how long (in seconds) to wait before giving up
    135     tick: length of time to wait between checks
    136 
    137   Returns:
    138     A tuple of handles to the process and the application's window,
    139     or (None, None) if it timed out waiting for the process
    140   """
    141 
    142   def EnumWindowProc(hwnd, ret):
    143     """Internal enumeration func, checks for visibility and proper PID."""
    144     if win32gui.IsWindowVisible(hwnd):  # don't bother even checking hidden wnds
    145       pid = win32process.GetWindowThreadProcessId(hwnd)[1]
    146       if pid == ret[0]:
    147         ret[1] = hwnd
    148         return 0    # 0 means stop enumeration
    149     return 1        # 1 means continue enumeration
    150 
    151   # We don't need to change anything about the startupinfo structure
    152   # (the default is quite sufficient) but we need to create it just the
    153   # same.
    154   sinfo = win32process.STARTUPINFO()
    155 
    156   proc = win32process.CreateProcess(
    157     path,                # path to new process's executable
    158     cmdline,             # application's command line
    159     None,                # process security attributes (default)
    160     None,                # thread security attributes (default)
    161     False,               # inherit parent's handles
    162     0,                   # creation flags
    163     None,                # environment variables
    164     None,                # directory
    165     sinfo)               # default startup info
    166 
    167   # Create process returns (prochandle, pid, threadhandle, tid). At
    168   # some point we may care about the other members, but for now, all
    169   # we're after is the pid
    170   pid = proc[2]
    171 
    172   # Enumeration APIs can take an arbitrary integer, usually a pointer,
    173   # to be passed to the enumeration function. We'll pass a pointer to
    174   # a structure containing the PID we're looking for, and an empty out
    175   # parameter to hold the found window ID
    176   ret = [pid, None]
    177 
    178   tries_until_timeout = timeout/tick
    179   num_tries = 0
    180 
    181   # Enumerate top-level windows, look for one with our PID
    182   while num_tries < tries_until_timeout and ret[1] is None:
    183     try:
    184       win32gui.EnumWindows(EnumWindowProc, ret)
    185     except pywintypes.error, e:
    186       # error 0 isn't an error, it just meant the enumeration was
    187       # terminated early
    188       if e[0]: raise e
    189 
    190     time.sleep(tick)
    191     num_tries += 1
    192 
    193   # TODO(jhaas): Should we throw an exception if we timeout? Or is returning
    194   # a window ID of None sufficient?
    195   return (proc[0], ret[1])
    196 
    197 
    198 def WaitForProcessExit(proc, timeout=None):
    199   """Waits for a given process to terminate.
    200 
    201   Args:
    202     proc: handle to process
    203     timeout: timeout (in seconds). None = wait indefinitely
    204 
    205   Returns:
    206     True if process ended, False if timed out
    207   """
    208   if timeout is None:
    209     timeout = win32event.INFINITE
    210   else:
    211     # convert sec to msec
    212     timeout *= 1000
    213 
    214   return (win32event.WaitForSingleObject(proc, timeout) ==
    215           win32event.WAIT_OBJECT_0)
    216 
    217 
    218 def WaitForThrobber(hwnd, rect=None, timeout=20, tick=0.1, done=10):
    219   """Wait for a browser's "throbber" (loading animation) to complete.
    220 
    221   Args:
    222     hwnd: window containing the throbber
    223     rect: rectangle of the throbber, in client coords. If None, whole window
    224     timeout: if the throbber is still throbbing after this long, give up
    225     tick: how often to check the throbber
    226     done: how long the throbber must be unmoving to be considered done
    227 
    228   Returns:
    229     Number of seconds waited, -1 if timed out
    230   """
    231   if not rect: rect = win32gui.GetClientRect(hwnd)
    232 
    233   # last_throbber will hold the results of the preceding scrape;
    234   # we'll compare it against the current scrape to see if we're throbbing
    235   last_throbber = ScrapeWindow(hwnd, rect)
    236   start_clock = time.clock()
    237   timeout_clock = start_clock + timeout
    238   last_changed_clock = start_clock;
    239 
    240   while time.clock() < timeout_clock:
    241     time.sleep(tick)
    242 
    243     current_throbber = ScrapeWindow(hwnd, rect)
    244     if current_throbber.tostring() != last_throbber.tostring():
    245       last_throbber = current_throbber
    246       last_changed_clock = time.clock()
    247     else:
    248       if time.clock() - last_changed_clock > done:
    249         return last_changed_clock - start_clock
    250 
    251   return -1
    252 
    253 
    254 def MoveAndSizeWindow(wnd, position=None, size=None, child=None):
    255   """Moves and/or resizes a window.
    256 
    257   Repositions and resizes a window. If a child window is provided,
    258   the parent window is resized so the child window has the given size
    259 
    260   Args:
    261     wnd: handle of the frame window
    262     position: new location for the frame window
    263     size: new size for the frame window (or the child window)
    264     child: handle of the child window
    265 
    266   Returns:
    267     None
    268   """
    269   rect = win32gui.GetWindowRect(wnd)
    270 
    271   if position is None: position = (rect[0], rect[1])
    272   if size is None:
    273     size = (rect[2]-rect[0], rect[3]-rect[1])
    274   elif child is not None:
    275     child_rect = win32gui.GetWindowRect(child)
    276     slop = (rect[2]-rect[0]-child_rect[2]+child_rect[0],
    277             rect[3]-rect[1]-child_rect[3]+child_rect[1])
    278     size = (size[0]+slop[0], size[1]+slop[1])
    279 
    280   win32gui.MoveWindow(wnd,          # window to move
    281                       position[0],  # new x coord
    282                       position[1],  # new y coord
    283                       size[0],      # new width
    284                       size[1],      # new height
    285                       True)         # repaint?
    286 
    287 
    288 def EndProcess(proc, code=0):
    289   """Ends a process.
    290 
    291   Wraps the OS TerminateProcess call for platform-independence
    292 
    293   Args:
    294     proc: process ID
    295     code: process exit code
    296 
    297   Returns:
    298     None
    299   """
    300   win32process.TerminateProcess(proc, code)
    301 
    302 
    303 def URLtoFilename(url, path=None, extension=None):
    304   """Converts a URL to a filename, given a path.
    305 
    306   This in theory could cause collisions if two URLs differ only
    307   in unprintable characters (eg. http://www.foo.com/?bar and
    308   http://www.foo.com/:bar. In practice this shouldn't be a problem.
    309 
    310   Args:
    311     url: The URL to convert
    312     path: path to the directory to store the file
    313     extension: string to append to filename
    314 
    315   Returns:
    316     filename
    317   """
    318   trans = string.maketrans(r'\/:*?"<>|', '_________')
    319 
    320   if path is None: path = ""
    321   if extension is None: extension = ""
    322   if len(path) > 0 and path[-1] != '\\': path += '\\'
    323   url = url.translate(trans)
    324   return "%s%s%s" % (path, url, extension)
    325 
    326 
    327 def PreparePath(path):
    328   """Ensures that a given path exists, making subdirectories if necessary.
    329 
    330   Args:
    331     path: fully-qualified path of directory to ensure exists
    332 
    333   Returns:
    334     None
    335   """
    336   try:
    337     os.makedirs(path)
    338   except OSError, e:
    339     if e[0] != 17: raise e   # error 17: path already exists
    340 
    341 
    342 def main():
    343   PreparePath(r"c:\sitecompare\scrapes\ie7")
    344   # We're being invoked rather than imported. Let's do some tests
    345 
    346   # Hardcode IE's location for the purpose of this test
    347   (proc, wnd) = InvokeAndWait(
    348     r"c:\program files\internet explorer\iexplore.exe")
    349 
    350   # Find the browser pane in the IE window
    351   browser = FindChildWindow(
    352     wnd, "TabWindowClass/Shell DocObject View/Internet Explorer_Server")
    353 
    354   # Move and size the window
    355   MoveAndSizeWindow(wnd, (0, 0), (1024, 768), browser)
    356 
    357   # Take a screenshot
    358   i = ScrapeWindow(browser)
    359 
    360   i.show()
    361 
    362   EndProcess(proc, 0)
    363 
    364 
    365 if __name__ == "__main__":
    366   sys.exit(main())
    367