Dmitry Karasik > Win32-GuiTest > Win32::GuiTest

Download:
Win32-GuiTest-1.60.tar.gz

Dependencies

Annotate this POD (1)

Related Modules

Win32::OLE
Win32::API
Win32::GUI
Win32::Clipboard
Win32::Process
WWW::Mechanize
HTTP::Recorder
Win32::Console
Test::Harness
Data::Dumper
more...
By perlmonks.org

CPAN RT

Open  0
View/Report Bugs
Module Version: 1.60   Source  

NAME ^

Win32::GuiTest - Perl GUI Test Utilities.

SYNOPSIS ^

  use Win32::GuiTest qw(FindWindowLike GetWindowText 
    SetForegroundWindow SendKeys);

  $Win32::GuiTest::debug = 0; # Set to "1" to enable verbose mode

  my @windows = FindWindowLike(0, "^Microsoft Excel", "^XLMAIN\$");
  for (@windows) {
      print "$_>\t'", GetWindowText($_), "'\n";
      SetForegroundWindow($_);
      SendKeys("%fn~a{TAB}b{TAB}{BS}{DOWN}");
  }

INSTALLATION ^

    // This batch file comes with MS Visual Studio.  Running
    // it first might help with various compilation problems.
    vcvars32.bat 

    perl makefile.pl
    nmake
    nmake test
    nmake install

    See more details in the DEVELOPMENT section elswhere in this document.

You can get the most recent release from http://www.sourceforge.net/projects/winguitest. The package will contain Win32-GuiTest.ppd file and Win32-GuiTest.tar.gz file, which is all that you need to use ppm. If you put those 2 files in C:\TEMP directory, the installation should look as follows. Enter PPM (Perl Package Manager) from the command-line and type commands as below

    C:\TEMP>ppm
    PPM interactive shell (2.0) - type 'help' for available commands.
    PPM> install C:\temp\win32-guitest.ppd
    Install package 'C:\temp\win32-guitest.ppd?' (y/N): Y
    Retrieving package 'C:\temp\win32-guitest.ppd'...
    Writing C:\Perl\site\lib\auto\Win32\GuiTest\.packlist
    PPM>

I extracted them to 'c:\temp', please use the directory where you extracted the files instead.

DESCRIPTION ^

Most GUI test scripts I have seen/written for Win32 use some variant of Visual Basic (e.g. MS-VB or MS-Visual Test). The main reason is the availability of the SendKeys function.

A nice way to drive Win32 programs from a test script is to use OLE Automation (ActiveX Scripting), but not all Win32 programs support this interface. That is where SendKeys comes handy.

Some time ago Al Williams published a Delphi version in Dr. Dobb's (http://www.ddj.com/ddj/1997/careers1/wil2.htm). I ported it to C and packaged it using h2xs...

The tentative name for this module is Win32::GuiTest (mostly because I plan to include more GUI testing functions).

I've created a Yahoo Group for the module that you can join at http://groups.yahoo.com/group/perlguitest/join

Also, an initial version of a script recording application has been written to use with this module. A copy of it may be found with this distribution (Recorder\Win32GuiTest.exe) or can be obtained at http://sourceforge.net/projects/winguitest

If the documentation of these functions is not satisfactory, you can try running a search on http://msdn.microsoft.com/ using the name of the function. Some of these functions are described there.

This distribution of the module - the one you are looking at now - has its own CVS repository at http://sourceforge.net/projects/winguitest Patches to both the code and the documentation are welcome.

Functions

$debug

When set enables the verbose mode.

SendKeys($keys[,$delay])

Sends keystrokes to the active window as if typed at the keyboard using the optional delay between key-up and key-down messages (default is 25 ms and should be OK for most uses).

The keystrokes to send are specified in KEYS. There are several characters that have special meaning. This allows sending control codes and modifiers:

        ~ means ENTER
        + means SHIFT 
        ^ means CTRL 
        % means ALT

The parens allow character grouping. You may group several characters, so that a specific keyboard modifier applies to all of them. Groups can be enclosed in groups.

E.g. SendKeys("ABC") is equivalent to SendKeys("+(abc)")

The curly braces are used to quote special characters (SendKeys("{+}{{}") sends a '+' and a '{'). You can also use them to specify certain named actions:

        Name          Action

        {BACKSPACE}   Backspace
        {BS}          Backspace
        {BKSP}        Backspace
        {BREAK}       Break
        {CAPS}        Caps Lock
        {DELETE}      Delete
        {DOWN}        Down arrow
        {END}         End
        {ENTER}       Enter (same as ~)
        {ESCAPE}      Escape
        {HELP}        Help key
        {HOME}        Home
        {INSERT}      Insert
        {LEFT}        Left arrow
        {NUMLOCK}     Num lock
        {PGDN}        Page down
        {PGUP}        Page up
        {PRTSCR}      Print screen
        {RIGHT}       Right arrow
        {SCROLL}      Scroll lock
        {TAB}         Tab
        {UP}          Up arrow
        {PAUSE}       Pause
        {F1}          Function Key 1
        ...           ...
        {F24}         Function Key 24
        {SPC}         Spacebar
        {SPACE}       Spacebar
        {SPACEBAR}    Spacebar
        {LWI}         Left Windows Key
        {RWI}         Right Windows Key 
        {APP}         Open Context Menu Key

or supply a number that will be treated as a VK code. Note that a single-digit number will be treated as a character, so prepend these with '0'.

All these named actions take an optional integer argument, like in {RIGHT 5}. For all of them, except PAUSE, the argument means a repeat count. For PAUSE it means the number of milliseconds SendKeys should pause before proceding.

In this implementation, SendKeys always returns after sending the keystrokes. There is no way to tell if an application has processed those keys when the function returns.

Unicode characters in $keys are translated into set of ALT+NUMPAD keystrokes. Note that not all applications can understand unicode input.

SendMouse($command)

This function emulates mouse input. The COMMAND parameter is a string containing one or more of the following substrings:

        {LEFTDOWN}    left button down
        {LEFTUP}      left button up
        {MIDDLEDOWN}  middle button down
        {MIDDLEUP}    middle button up
        {RIGHTDOWN}   right button down
        {RIGHTUP}     right button up
        {LEFTCLICK}   left button single click
        {MIDDLECLICK} middle button single click
        {RIGHTCLICK}  right button single click
        {ABSx,y}      move to absolute coordinate ( x, y )
        {RELx,y}      move to relative coordinate ( x, y )

Note: Absolute mouse coordinates range from 0 to 65535. Relative coordinates can be positive or negative. If you need pixel coordinates you can use MouseMoveAbsPix.

Also equivalent low-level functions are available:

    SendLButtonUp()
    SendLButtonDown()
    SendMButtonUp()
    SendMButtonDown()
    SendRButtonUp()
    SendRButtonDown()
    SendMouseMoveRel(x,y)
    SendMouseMoveAbs(x,y)
MouseMoveAbsPix($x,$y)

Move the mouse cursor to the screen pixel indicated as parameter.

  # Moves to x=200, y=100 in pixel coordinates.
  MouseMoveAbsPix(200, 100);
MouseMoveWheel($change)
  Positive or negative value to direct mouse wheel movement.
FindWindowLike($window,$titleregex,$classregex,$childid,$maxlevel)

Finds the window handles of the windows matching the specified parameters and returns them as a list.

You may specify the handle of the window to search under. The routine searches through all of this windows children and their children recursively. If 'undef' then the routine searches through all windows. There is also a regexp used to match against the text in the window caption and another regexp used to match against the text in the window class. If you pass a child ID number, the functions will only match windows with this id. In each case undef matches everything.

GetWindowID($window)
    Returns the control Id of the specified window.
PushButton($button[,$delay])

Equivalent to

    PushChildButton(GetForegroundWindow, BUTTON, DELAY)
PushChildButton($parent,$button[,$delay])

Allows generating a mouse click on a particular button.

parent - the parent window of the button

button - either the text in a button (e.g. "Yes") or the control ID of a button.

delay - the time (0.25 means 250 ms) to wait between the mouse down and the mouse up event. This is useful for debugging.

PushChildById( $parent, $button, $level, $delay )

Allows pushing a button, which control id is eqaul to a given parameter. PushChildButton tries to match parameter against control id or caption. PushChildById matches only against control id. Secondly, PushChildById allows specifying search depth in the windows hierarchy tree. The default is 2, which means that only direct children will be pushed.

WaitWindowLike($parent,$wndtitle,$wndclass,$wndid,$depth,$wait)

Function which allows one to wait for a window to appear vs. using hard waits (e.g. sleep 2).

parent - Where to start (parent window)

wndtitle - Regexp for the window title

wndclass - Regexp for the window class name

wndid - Numeric Window or Control ID

depth - How deep should we search before we stop

wait - How many seconds should we wait before giving up

WaitWindow($wndtitle,[$wait])

Minimal version of WaitWindowLike. Only requires the window title regexp. You can also specify the wait timeout in seconds.

wndtitle - Regexp for the window title

wait - How many seconds should we wait before giving up

IsWindowStyle($window, $style)
    Determines if a window has the specified style.  See sample
    script for more details.
IsWindowStyleEx($window, $exstyle)
    Determines if a window has the specified extended
    style.  See sample script for more details.
GetMenu

Using the corresponding library function (see MSDN) it returns a MenuID number

GetMenuItemIndex($curr, $menu);

$curr is a MenuId and $menu is the (localized !) name of the menu including the hot key: "Rep&eate" Returns the index of the menu item (-1 if not found)

GetMenuItemCount($menu)

Returns the number of elements in the given menu.

MenuSelect($menupath,$window,$menu)

Allows selecting a menu programmatically.

Simple Examples: # Exit foreground application through application menu. MenuSelect("&File|E&xit");

    # Exit foreground application through system menu
    MenuSelect("&Close", 0, GetSystemMenu(GetForegroundWindow(), FALSE));
GetMenuItemInfo($menuHndl, $cnt)

Receives a menu handler (one we got from GetMenu or GetSubMenu) and a number (which is the location of the item within the given menu).

Returns a hash of which there are currently 2 keys: type can be either "string" or "separator" - this is the type of the menu item text is the visible text of the menu item (provided only for "string" type)

WARNING: This is an experimental function. Its behavior might change.

MouseClick($window [,$parent] [,$x_offset] [,$y_offset] [,$button] [,$delay])

Allows one to easily interact with an application through mouse emulation.

window = Regexp for a Window caption / Child caption, or just a Child ID.

parent = Handle to parent window. Default is foreground window. Use GetDesktopWindow() return value for this if clicking on an application title bar.

x_offset = Offset for X axis. Default is 0.

y_offset = Offset for Y axis. Default is 0.

button = {LEFT}, {MIDDLE}, {RIGHT}. Default is {LEFT}

delay = Default is 0. 0.50 = 500 ms. Delay between button down and button up.

Simple Examples:

    # Click on CE button if its parent window is in foreground.
    MouseClick('^CE$');

    # Right click on CE button if its parent window is in foreground
    MouseClick('^CE$', undef, undef, undef, '{RIGHT}');

    # Click on 8 button window under the specified parent window; where
    # [PARENTHWND] will be replaced by a parent handle variable.
    MouseClick('8', [PARENTHWND]);

    # Click on Calculator parent window itself
    MouseClick('Calculator', GetDesktopWindow());
$buf_str = AllocateVirtualBuffer( $hwnd, $size )

Allocates memory in the address space of the process, which is an owner of a window identified by $hwnd. Returns a reference to a hash, which has 2 elements:

ptr - address of the allocated memory
process - process handle (in the Win32 meaning, as returned by Win32 OpenProcess API function
$value = ReadFromVirtualBuffer( $buf_str, $size )

Read from a memory in the address space of the other process. $buf_str is a reference to a hash returned by AllocateVirtualBuffer.

Returns read value.

WriteToVirtualBuffer( $buf_str, $value )

Write to a memory in the address space of the other process. $buf_str is a reference to a hash returned by AllocateVirtualBuffer. $value is a value to be copied.

FreeVirtualBuffer( $buf_str )

Frees memory allocated by AllocateVirtualBuffer

$text = WMGetText($hwnd) *

Sends a WM_GETTEXT to a window and returns its contents

$set = WMSetText(hwnd,text) *

Sends a WM_SETTEXT to a window setting its contents

($x,$y) = GetCursorPos() *

Retrieves the cursor's position,in screen coordinates as (x,y) array.

GetCaretPos()

Retrieves the caret's position, in client coordinates as (x,y) array. (Like Windows function)

HWND SetFocus(hWnd)

Sets the keyboard focus to the specified window

HWND GetDesktopWindow() *

Returns a handle to the desktop window

HWND GetWindow(hwnd,uCmd) *
SV * GetWindowText(hwnd) *

Get the text name of the window as shown on the top of it. Beware, this is text depends on localization.

$class = GetClassName(hwnd) *

Using the same Windows library function returns the name of the class wo which the specified window belongs.

See MSDN for more details.

You can also check out MSDN to see an overview of the Window Classes.

HWND GetParent(hwnd) *

A library function (see MSDN) to return the WindowID of the parent window. See MSDN for the special cases.

long GetWindowLong(hwnd,index) *
BOOL SetForegroundWindow(hWnd) *

See corresponding Windows functions.

@wnds = GetChildWindows(hWnd)

Using EnumChildWindows library function (see MSDN) it returns the WindowID of each child window. If the children have their own children the function returns them too until the tree ends.

BOOL IsChild(hWndParent,hWnd) *

Using the corresponding library function (see MSDN) it returns true if the second window is an immediate child or a descendant window of the first window.

$depth = GetChildDepth(hAncestor,hChild)

Using the GetParent library function in a loop, returns the distance between an ancestor window and a child (descendant) window.

Features/bugs: If the given "ancsetor" is not really an ancestor, the return value is the distance of child from the root window (0) If you supply the same id for both the ancestor and the child you get 1. If the ancestor you are checking is not 0 then the distance given is 1 larger than it should be.

see eg\get_child_depth.pl

$res = SendMessage(hWnd,Msg,wParam,lParam) *

This is a library function (see MSDN) used by a number of the functions provided by Win32::GuiTest. It sends the specified message to a window or windows. HWnd is the WindowID or HWND_BROADCAST to send message to all top level windows. Message is not sent to child windows. (If I understand this correctly this means it is sent to all the immediate children of the root window (0). Msg the message wParam additional parameter lParam additioanl parameter

It is most likely you won't use this directly but through one of the functions implemented already in Win32::GuiTest.

See the guitest.xs for some examples.

$res = PostMessage(hwnd,msg,wParam,lParam) *

See corresponding Windows library function in MSDN.

CheckButton(hwnd)
UnCheckButton(hwnd)
GrayOutButton(hwnd)
BOOL IsCheckedButton(hwnd)
BOOL IsGrayedButton(hwnd)

The names say it. Works on radio buttons and checkboxes. For regular buttons, use IsWindowEnabled.

BOOL IsWindow(hwnd) *
($x,$y) = ScreenToClient(hwnd,x,y) *
($x,$y) = ClientToScreen(hwnd,x,y) *
($x,$y) = GetCaretPos(hwnd) *A
HWND SetFocus(hWnd) *A
HWND GetFocus(hwnd) *A
HWND GetActiveWindow(hwnd) *A
HWND GetForegroundWindow() *
HWND SetActiveWindow(hwnd) *A
BOOL EnableWindow(hwnd,fEnable) *
BOOL IsWindowEnabled(hwnd)*
BOOL IsWindowVisible(hwnd)*
BOOL ShowWindow(hwnd,nCmdShow) *A

See corresponding Windows functions.

($x,$y) = ScreenToNorm(x,y)

Returns normalised coordinates of given point (0-FFFF as a fraction of screen resolution)

($x,$y) = NormToScreen(x,y)

The opposite transformation

($x,$y) = GetScreenRes()

Returns screen resolution

HWND WindowFromPoint(x, y)
($l,$t,$r,$b) = GetWindowRect(hWnd) *
($l,$t,$r,$b) = GetClientRect(hWnd) *

See corresponding Windows functions.

SelComboItem($window, $index)

Selects an item in the combo box based off an index (zero-based).

SelComboItemText($window, $txt)

Selects an item in the combo box based off text (case insensitive).

$txt = GetComboText(hwnd,index)
$txt = GetListText(hwnd,index)
@lst = GetComboContents(hWnd)
@lst = GetListContents(hWnd)

Fetch the contents of the list and combo boxes.

GetAsyncKeyState($key)
IsKeyPressed($key)

Wrapper around the GetAsyncKeyState API function. Returns TRUE if the user presses the specified key.

    IsKeyPressed("ESC");
    IsKeyPressed("A");
    IsKeyPressed("DOWN"); 
    IsKeyPressed( VK_DOWN);
SendRawKey($virtualkey,$flags)

Wrapper around keybd_event. Allows sending low-level keys. The first argument is any of the VK_* constants. The second argument can be 0, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP or a combination of them.

    KEYEVENTF_EXTENDEDKEY - Means it is an extended key (i.e. to distinguish between arrow keys on the numeric keypad and elsewhere). 
    KEYEVENTF_KEYUP       - Means keyup. Unspecified means keydown.

   #Example
   use Win32::GuiTest qw/:FUNC :VK/;

   while (1) {
       SendRawKey(VK_DOWN, KEYEVENTF_EXTENDEDKEY); 
       SendKeys "{PAUSE 200}";
   }
VkKeyScan(int)
GetListViewContents($handle)
    Return the items of the list view with C<$handle> as a list, each
        element of which is a reference to an array containing the values
        in each column.
SelListViewItem($window, $idx, [$multi_select])
    Selects an item in the list view based off an index (zero-based).

        # Select first item, clears out any previous selections.
        SelListViewItem($win, 0);
        # Select an *additional* item.
        SelListViewItem($win, 1, 1);
SelListViewItemText($window, $txt, [$multi_select])
    Selects an item in the list view based off text (case insensitive).

        # Select first item, clears out any previous selections.
        SelListViewItemText($win, 'Temp');
        # Select an *additional* item.
        SelListViewItemText($win, 'cabs', 1);
IsListViewItemSel($window, $txt)
   Determines if the specified list view item is selected.
GetTabItems($window)
    Returns a list of a tab control's labels.
SelTabItem($window, $idx)
    Selects a tab based off an index (zero-based).
SelTabItemText($window, $txt)
    Selects a tab based off text label (case insensitive).
IsTabItemSel($window, $txt)
   Determines if the specified tab item is selected.
SelTreeViewItemPath($window, $path)
    Selects a tree view item based off a "path" (case insensitive).

    # Select Machine item and Processors sub-item.
    SelTreeViewItemPath($window, "Machine|Processors");

    SelTreeViewItemPath($window, "Item");
GetTreeViewSelPath($window)
   Returns a string containing the path (i.e., "parent|child") of
   the currently selected tree view item.

   $oldpath = GetTreeViewSelPath($window);
   SelTreeViewItemPath($window, "Parent|Child");
   SelTreeViewItemPath($window, $oldpath);
$hpopup = GetPopupHandle($hwnd, $x, $y, [$wait])
   This function gets the handle of a popup window generated by
   right-clicking at the $x and $y screen coordinates (absolute). An
   optional delay can be entered which will wait the given number of
   milliseconds after the right-click for the window to appear (default
   is 50). Zero is returned when no popup menu is found.

DibSect

A class to manage a Windows DIB section. Currently limited in functionality to 24-bit images. Pulled from old code into GuiTest when I (jurasz@imb.uni-karlsruhe.de) needed to create several grayscale screen dumps.

Possible future extenstions: other color resolutions, loading, comparison of bitmaps, getting from clipboard.

Synopsis:

  $ds = new Win32::GuiTest::DibSect;
  $ds->CopyWindow($w);
  $ds->ToGrayScale();
  $ds->SaveAs("bla.bmp");
  $ds->ToClipboard();
bool DibSect::CopyClient(hwnd,[rect])

Copy a client area of given window (or possibly its subset) into a given DibSect. The rectangle may be optionally passed as a reference to 4-element array. To get the right result make sure the window you want to copy is not obscured by others.

bool DibSect::CopyWindow(hwnd)

Copy the window rectangle. Equivalent to

  $ds->CopyClient(GetDesktopWindow(), \@{[GetWindowRect($w)]});
bool DibSect::SaveAs(szFile)

Save the current contents of the DIB section in a given file. With 24-bit resolution it can grow quite big, so I immediately convert them to PNG (direct writing of PNG seemed to complicated to implement).

bool DibSect::Invert()

Invert the colors in a current DIB section.

bool DibSect::ToGrayScale()

Convert the DibSection to the gray scale. Note that it is still encoded as 24-bit BMP for simplicity.

bool DibSect::ToClipboard()

Copies the DibSect to clipboard (as an old-fashioned metafile), so that it can be further processed with your favourite image processing software, for example automatically using SendKeys.

bool DibSect::Destroy()

Destroys the contents of the DIB section.

UNICODE SUPPORT ^

Currently (2007) there's no consensus about unicode input in Perl, so the module declares function UnicodeSemantics that sets whether information queried from windows should use A or W syscalls. The function that support this differentiation, and produce different results depending on value set to UnicodeSemantics is:

GetWindowText, and all its callers, - FindWindowLike, WaitWindow, WaitWindowLike

SendKeys translated unicode characters into set of ALT+NUMPAD keystrokes. Note that not all applications can understand unicode input.

UnicodeSemantics [BOOL]

If a boolean parameter is set, changes the semantics flag for functions that return results of either A or W syscalls. If the parameter is not set, returns the current value of the flag.

DEVELOPMENT ^

If you would like to participate in the development of this module there are several thing that need to be done. For some of them you only need Perl and the latest source of the module from CVS for others you'll also need to have a C++ compiler.

To get the latest source code you need a CVS client and then do the following:

 cvs -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/winguitest login
 cvs -z3 -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/winguitest co Win32-GuiTest

See more detailed explanations here http://sourceforge.net/projects/winguitest/

cygwin

g++ needs to be installed

  perl Makefile.PL
  make
  make test
  make install

MSVC environment

To setup a development environment for compiling the C++ code you can either buy Visual Studio with Visual C++ or you can download a few things free of charge from Microsoft. There might be other ways too we have not explored.

The instructions to get the free environment are here:

From http://www.microsoft.com/ download and install:

 1) Microsoft .NET Framework Version 1.1 Redistributable Package
 2) .NET Framework SDK Version 1.1

This is not enough as there are a number of header files and libraries that are not included in these distributions. You can get them from Microsoft in two additional downloads. For these you will have to be using Internet Explorer. Visit

  http://www.microsoft.com/msdownload/platformsdk/sdkupdate/

and install

 1) Core SDK
 2) Microsoft Data Access Components 2.7

Before you can compile you'll have to open a command prompt and execute the sdkvars.bat script from the.NET SDK that will set a number of environment variables. In addition you'll have to run the setenv.bat you got with the Core SDK (and located in C:\Program Files\Microsoft SDK) with the appropriate parameters. For me this was /XP32 /RETAIL

In order to finish the packaging you'll also need the tar, gzip and zip utilities from

 http://gnuwin32.sourceforge.net/packages.html

I have not tried it yet.

After this you will probably be able to do the normal cycle:

 perl makefile.pl
 nmake
 nmake test

 or run

 perl makedist.pl

SEE ALSO ^

Module's documentation is available at http://www.piotrkaluski.com/files/winguitest/docs/index.html.

TODO ^

Here are a few items where help would be welcome.

Perl only

 Improve Tests
 Improve documentation
 Add more examples and explain them

C++ compiler needed

 Add more calls to the C++ backend
 Fix current calls

 32bit custom controls (some already implemented)
 Possibly Java interfaces
 Retreive the list of the menu of a given window.

COPYRIGHT ^

The SendKeys function is based on the Delphi sourcecode published by Al Williams <http://www.al-williams.com/awc/> in Dr.Dobbs <http://www.ddj.com/ddj/1997/careers1/wil2.htm>.

Copyright (c) 1998-2002 Ernesto Guisado, (c) 2004 Dennis K. Paulsen. All rights reserved. This program is free software; You may distribute it and/or modify it under the same terms as Perl itself.

AUTHORS ^

Ernesto Guisado (erngui@acm.org), http://triumvir.org

Jarek Jurasz (jurasz@imb.uni-karlsruhe.de), http://www.uni-karlsruhe.de/~gm07 wrote DibSect and some other pieces (see Changes for details).

Dennis K. Paulsen (ctrondlp@cpan.org) wrote various pieces (See Changes for details).

Dmitry Karasik (dmitry@karasik.eu.org) added support for unicode and cygwin/mingw.

CREDITS ^

Thanks very much to:

Johannes Maehner
Ben Shern
Phill Wolf
Mauro
Sohrab Niramwalla
Frank van Dijk
Jarek Jurasz
Wilson P. Snyder II
Rudi Farkas
Paul Covington
Piotr Kaluski
...and more...

for code, suggestions and bug fixes.

syntax highlighting: