=begin license --------------------------------------------------------------------------- Copyright (c) 2004-2006, Paul Rogers and Bret Pettichord All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the names Paul Rogers, nor Bret Pettichord nor the names of any other contributors to this software may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------- (based on BSD Open Source License) =end =begin rdoc This is Watir's window clicker helper class, uses Win32 api calls to access buttons and windows. Typical usage: # include this file in your script require "watir/winClicker.rb" # create a new instance of WinClicker and use it wc = WinClicker.new wc.clickWindowsButton("My Window", "Click Me", 30) =end # # class to click javascript dialog boxes, file requester dialogs etc require 'dl/import' require 'dl/struct' require "timeout" require 'Win32API' class WinClicker WM_CLOSE = 0x0010 WM_KEYDOWN = 0x0100 WM_KEYUP = 0x0101 WM_CHAR = 0x0102 BM_CLICK = 0x00F5 WM_COMMAND = 0x0111 WM_SETTEXT = 0x000C WM_GETTEXT = 0x000D HWND_TOP = 0 HWND_BOTTOM = 1 HWND_TOPMOST = -1 HWND_NOTOPMOST = -2 SWP_SHOWWINDOW = 0x40 SWP_NOSIZE = 1 SWP_NOMOVE = 2 TRUE_1 = 1 # these are constants for commonly used windows windows WINCLASS_DIALOG = "32770" # these are the most used methods def initialize @User32 = DL.dlopen("user32") # we must determine the path we are in @path_to_clicker = '"' + File.expand_path(File.dirname(__FILE__)) + '"' end # The system function passes command to the command interpreter, which executes the string as an operating-system command # http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vccore98/html/_crt_system.2c_._wsystem.asp # using win32api def winsystem(command) pid = Win32API.new("crtdll", "system", ['P'], 'L').Call(command) end # returns the short path version of a long path # 8.3 style def getShortFileName(longName) size = 255 buffer = " " * 255 returnSize = Win32API.new("kernel32" , "GetShortPathNameA" , 'ppl' , 'L').Call(longName , buffer , size ) a = "" a = a + buffer[0...returnSize] return a end # Set the first edit box in the Choose file dialog to textToSet # we may need to play with the default try count. 3 is a reasonably safe value. def setFileRequesterFileName( textToSet, tryCount = 3 ) for i in (1..tryCount) # first set the Choose File Window to be active hWnd = getWindowHandle("Choose file" ) if hWnd != -1 makeWindowActive(hWnd) setTextValueForFileNameField( hWnd , textToSet) clickWindowsButton_hwnd(hWnd, "&Open") return true end end return false end # fire off setting the file name for the Choose file dialog # in a new process def setFileRequesterFileName_newProcess ( textToSet ) myapp = "rubyw #{@path_to_clicker}/setFileDialog.rb #{textToSet}" # first argument to system call is a window title, in this case blank "" winsystem( "start \"\" #{myapp}" ) end # Return the text value from the first combo box # on the Choose file dialog or nil if not found def getFileRequesterFileName() # first set the Choose File Window to be active hWnd = getWindowHandle("Choose file" ) if hWnd != -1 makeWindowActive(hWnd) return getTextValueForFileNameField( hWnd ) else return nil end end # Click on a dialog with title of "Internet Explorer" # Default button to click is "OK" # parenthWnd not used def clickJavaScriptDialog(button="OK" , parenthWnd = -1) clickWindowsButton("Internet Explorer" , button ) end # Calls system to launch a new process to click on the button # defaults to "OK" button def clickJSDialog_NewProcess(button = "OK" ) myapp = "rubyw #{@path_to_clicker}/clickJSDialog.rb #{button}" log "Starting win clicker in a new process. Looking for button #{button}" log "Starting app: #{myapp}" # first argument to system call is a window title, in this case blank "" winsystem( "start \"\" #{myapp}" ) end # as a thread def clickJSDialog_Thread(button = "OK" ) sleep 3 n = 0 while n < 3 sleep 1 clickWindowsButton("Internet Explorer" , button ) n=n+1 end end # Looks for a window titled "Security Alert", clicks # on Yes button def clearSecurityAlertBox clickWindowsButton("Security Alert" , "&Yes" ) end alias :clear_security_alert :clearSecurityAlertBox # Returns the parent handle for the given child handle def getParent (childhWnd ) # pass a hWnd into this function and it will return the parent hWnd getParentWindow = @User32['GetParent' , 'II' ] a , b = getParentWindow.call(childhWnd ) return a end alias :get_parent :getParent def with_dl_callback(type, prc) callback = DL.callback(type, &prc) error = nil begin yield callback ensure DL.remove_callback(callback) end end # Enumerates open windows and # returns a window handle from a given title and window class # Window class and title are matched regexes def getWindowHandle(title, winclass = "" ) enum_windows = @User32['EnumWindows', 'IPL'] get_class_name = @User32['GetClassName', 'ILpI'] get_caption_length = @User32['GetWindowTextLengthA' ,'LI' ] # format here - return value type (Long) followed by parameter types - int in this case - see http://www.ruby-lang.org/cgi-bin/cvsweb.cgi/~checkout~/ruby/ext/dl/doc/dl.txt? get_caption = @User32['GetWindowTextA', 'iLsL' ] len = 32 buff = " " * len classMatch = false bContinueEnum = -1 # Windows "true" to continue enum_windows. found_hwnd = -1 enum_windows_proc = lambda {|hwnd,lparam| sleep 0.05 r,rs = get_class_name.call(hwnd, buff, buff.size) if winclass != "" then if /#{winclass}/ =~ rs[1].to_s classMatch = true end else classMatch = true end if classMatch ==true textLength, a = get_caption_length.call(hwnd) captionBuffer = " " * (textLength+1) t , textCaption = get_caption.call(hwnd, captionBuffer , textLength+1) if /#{title}/ =~ textCaption[1].to_s found_hwnd = hwnd bContinueEnum = 0 # False, discontinue enum_windows end bContinueEnum else bContinueEnum end } with_dl_callback('ILL',enum_windows_proc) do |callback| r,rs = enum_windows.call(callback, 0) end return found_hwnd end alias :get_window_handle :getWindowHandle # Call SwitchToThisWindow win32api which will # The SwitchToThisWindow function is called to switch focus to a specified window # and bring it to the foreground def makeWindowActive (hWnd) switch_to_window = @User32['SwitchToThisWindow' , 'pLI' ] # set it to be the one with focus switch_to_window.call(hWnd , 1) end alias :make_window_active :makeWindowActive # Posts a message to the handle passed in to click def clickButtonWithHandle(buttonhWnd) post_message = @User32['PostMessage', 'ILILL'] r,rs = post_message.call(buttonhWnd, BM_CLICK, 0, 0) end alias :click_button_with_handle :clickButtonWithHandle # Based on the parent window handle passed in, # click on the button with the given caption. def clickWindowsButton_hwnd (hwnd , buttonCaption ) makeWindowActive(hwnd) d = getChildHandle( hwnd , buttonCaption ) if d != -1 makeWindowActive(hwnd) clickButtonWithHandle(d) else return false end return true end alias :click_windows_button_hwnd :clickWindowsButton_hwnd # this clicks the button with the name in the window with the caption. It keeps looking for the button until # until the timeout expires def clickWindowsButton (windowCaption , buttonCaption , maxWaitTime=30 ) sleep 1 hwnd = -1 begin timeout(maxWaitTime) do hwnd = getWindowHandle(windowCaption) while hwnd == -1 hwnd = getWindowHandle(windowCaption) sleep 0.5 end makeWindowActive(hwnd) end rescue Timeout::Error return false rescue => e raise e end if hwnd != -1 makeWindowActive(hwnd) else end d = getChildHandle( hwnd , buttonCaption ) if d != -1 makeWindowActive(hwnd) clickButtonWithHandle(d) else return false end return true end alias :click_windows_button :clickWindowsButton # Enumerate through children of the parent hwnd, pass back # the handle for the control with the given caption # the caption is compared as a regex def getChildHandle ( hWnd , childCaption ) enum_childWindows = @User32['EnumChildWindows' , 'IIPL' ] get_caption_length = @User32['GetWindowTextLengthA' ,'LI' ] # format here - return value type (Long) followed by parameter types - int in this case - see http://www.ruby-lang.org/cgi-bin/cvsweb.cgi/~checkout~/ruby/ext/dl/doc/dl.txt? get_caption = @User32['GetWindowTextA', 'iLsL' ] match_hwnd = -1 # hWnd of handle matching childCaption buff = " " * 16 get_class_name = @User32['GetClassName', 'ILpI'] bContinueEnum = -1 enum_childWindowsProc = lambda {|chwnd,lparam| r,rs = get_class_name.call(chwnd, buff, buff.size) textLength, a = get_caption_length.call(chwnd) captionBuffer = " " * (textLength+1) t , textCaption = get_caption.call(chwnd, captionBuffer , textLength+1) if /#{childCaption}/ =~ textCaption[1].to_s then match_hwnd = chwnd bContinueEnum = 0 # Windows "false" to discontinue enum_childWindow end bContinueEnum } with_dl_callback('ILL',enum_childWindowsProc) do |callback| r = enum_childWindows.call(hWnd, callback ,0) end return match_hwnd end alias :get_chwnd :getChildHandle # Convenience method to return Static text for # children of the window with the given caption def getStaticText(caption) return getStaticTextFromWindow(caption, -1) end alias :get_static_text :getStaticText # Convenience method to return Static text for # children of the window handle def getStaticText_hWnd (hWnd) return getStaticTextFromWindow("" , hWnd) end alias :get_static_text_hwnd :getStaticText_hWnd # Return text as an array from child controls of the window # given as either a handle or with the given caption # that have a class type of Static def getStaticTextFromWindow( windowCaption , hWnd) enum_childWindows = @User32['EnumChildWindows' , 'IIPL' ] get_caption_length = @User32['GetWindowTextLengthA' ,'LI' ] # format here - return value type (Long) followed by parameter types - int in this case - see http://www.ruby-lang.org/cgi-bin/cvsweb.cgi/~checkout~/ruby/ext/dl/doc/dl.txt? get_caption = @User32['GetWindowTextA', 'iLsL' ] staticText = [] buff = " " * 16 get_class_name = @User32['GetClassName', 'ILpI'] if hWnd == -1 hWnd = getWindowHandle(windowCaption) end if hWnd == -1 return staticText end bContinueEnum = -1 enum_childWindows_proc = lambda {|hWnd,lparam| r,rs = get_class_name.call(hWnd, buff, buff.size) if rs[1].to_s == "Static" # there must be a better way of detecting this textLength, a = get_caption_length.call(hWnd) captionBuffer = " " * (textLength+1) t , textCaption = get_caption.call(hWnd, captionBuffer , textLength+1) staticText << textCaption[1].to_s end bContinueEnum } with_dl_callback('ILL',enum_childWindows_proc) do |callback| r = enum_childWindows.call(hWnd, callback ,0) end return staticText end alias :get_static_text_from_window :getStaticTextFromWindow # returns the handle (or -1 if its not found) of the # nth control of this class in the parent window specified # by the window handle def getHandleOfControl (hWnd , controlClass, position ) enum_childWindows = @User32['EnumChildWindows' , 'IIPL' ] get_caption_length = @User32['GetWindowTextLengthA' ,'LI' ] # format here - return value type (Long) followed by parameter types - int in this case - see http://www.ruby-lang.org/cgi-bin/cvsweb.cgi/~checkout~/ruby/ext/dl/doc/dl.txt? get_caption = @User32['GetWindowTextA', 'iLsL' ] control_hWnd = [] buff = " " * 16 get_class_name = @User32['GetClassName', 'ILpI'] bContinueEnum = -1 enum_childWindows_proc = lambda {|hWnd,lparam| r,rs = get_class_name.call(hWnd, buff, buff.size) if rs[1].to_s == controlClass # there must be a better way of detecting this control_hWnd << hWnd end bContinueEnum } with_dl_callback('ILL',enum_childWindows_proc) do |callback| r = enum_childWindows.call(hWnd, callback ,0) end controlHwnd = control_hWnd[position] if controlHwnd == nil then controlHwnd = -1 end return controlHwnd end alias :get_handle_of_ctrl :getHandleOfControl # Call set text on the given window handle def setComboBoxText(hWnd , textToSet) set_text(hWnd, textToSet) end alias :set_combo_txt :setComboBoxText # Call set text on the given window handle def setTextBoxText(hWnd , textToSet) set_text(hWnd, textToSet) end alias :set_textbox_txt :setTextBoxText # Private method to set text called by the two methods above def set_text(hWnd, textToSet) send_message = @User32['SendMessage', 'ILISS'] r ,rs = send_message.call(hWnd , WM_SETTEXT ,'',textToSet) end private :set_text # Get the text in the handle for the given control def getControlText(hWnd) buff = " " * 256 send_message = @User32['SendMessage', 'ILIIS'] r ,rs = send_message.call(hWnd , WM_GETTEXT , 256 , buff ) return buff.to_s end alias :get_ctrl_txt :getControlText # get the title for the specified hwnd def getWindowTitle(hWnd) buff = " " * 256 getWindowText = @User32['GetWindowText' , 'ILSI'] r , rs = getWindowText.call( hWnd , buff , 256 ) return buff.to_s end alias :get_win_title :getWindowTitle # Get the text in the first combo box # file requester methods returns nil on failure to # locate the 1st combobox def getTextValueForFileNameField(parenthWnd) f = getHandleOfControl(parenthWnd, "ComboBox", 1) if f == -1 then # unable to find the first combobox return nil else # we have the control and now # can send it some messages return getWinText(f ) end end alias :get_file_name :getTextValueForFileNameField # this sets the filename field to text to set def setTextValueForFileNameField( parenthWnd , textToSet ) # get the handle of the nth control that is an Edit box f = getHandleOfControl(parenthWnd, "Edit" , 0 ) if f == -1 then # unable to get a handle on the first edit control return false else # we found the control and can now send it some messages setComboBoxText(f , textToSet) return true end end alias :set_file_name :setTextValueForFileNameField end