Skip to content

Win32COM Automation

Python
# =============================================================================
# DEVELOPMENT MODE TOGGLE
# =============================================================================
USE_LOCAL_SOURCE = False  # <-- TOGGLE THIS

if USE_LOCAL_SOURCE:
    import sys
    from pathlib import Path
    local_path = str(Path.cwd().parent)
    if local_path not in sys.path:
        sys.path.insert(0, local_path)
    print(f"📁 LOCAL SOURCE MODE: Loading from {local_path}/ras_commander")
else:
    print("📦 PIP PACKAGE MODE: Loading installed ras-commander")

# Import ras-commander
from ras_commander import RasExamples, RasGuiAutomation, RasMap, init_ras_project, ras

# Additional imports
import os
import sys
from pathlib import Path

# Verify which version loaded
import ras_commander
print(f"✓ Loaded: {ras_commander.__file__}")

When GUI Automation Is Actually Needed

Scenario 1: Testing GUI Features - Verifying menu items exist - Testing dialog box behavior - Debugging GUI-specific bugs

Scenario 2: Tasks Without API - Exporting to Google Earth (no API yet) - Creating schematic plots (limited API) - Accessing advanced RAS Mapper features

Scenario 3: Legacy Integration - Integrating with non-Python tools that expect GUI - Screen recording demonstrations - Training materials creation

RasGuiAutomation Helper Functions

ras-commander provides GUI automation utilities:

Python
from ras_commander import RasGuiAutomation

# Find HEC-RAS window
hwnd = RasGuiAutomation.find_hecras_window()

# Get window handle by title
window = RasGuiAutomation.get_window_by_title("HEC-RAS")

# Send keyboard input
RasGuiAutomation.send_keys(hwnd, "Alt+F")  # File menu

Debugging GUI Automation

Common Issues:

  1. Window Not Found: HEC-RAS not running or title changed

    Python
    import win32gui
    
    def list_windows():
        def callback(hwnd, windows):
            if win32gui.IsWindowVisible(hwnd):
                windows.append((hwnd, win32gui.GetWindowText(hwnd)))
            return True
    
        windows = []
        win32gui.EnumWindows(callback, windows)
        return windows
    
    # Find HEC-RAS window
    all_windows = list_windows()
    ras_windows = [w for w in all_windows if 'RAS' in w[1]]
    print(ras_windows)
    

  2. Timing Issues: GUI not ready for next action

    Python
    import time
    
    # Click button
    button.click()
    time.sleep(1)  # Wait for action to complete
    
    # Better: Wait for element to appear
    app.window().wait('ready', timeout=10)
    

  3. Element Not Found: Control identifiers changed

    Python
    # Use pywinauto's print_control_identifiers()
    app = Application().connect(title_re=".*HEC-RAS.*")
    app.window().print_control_identifiers()
    

Prerequisites

Before running this notebook, ensure you have:

  1. ras-commander installed: pip install ras-commander
  2. Python 3.10+: Check with python --version
  3. HEC-RAS installed: GUI automation requires running application
  4. pywin32: pip install pywin32
  5. pywinauto: pip install pywinauto (for advanced automation)
  6. Windows OS: GUI automation is Windows-specific

What You'll Learn

This notebook demonstrates Windows GUI automation for HEC-RAS:

  • win32gui: Low-level window manipulation
  • pywinauto: High-level GUI automation
  • RasGuiAutomation: ras-commander's GUI helper functions
  • 121_legacy_hecrascontroller_and_rascontrol.ipynb - COM-based automation (HEC-RAS 3.x-5.x)
  • 110_single_plan_execution.ipynb - Preferred API-based execution (no GUI)

Critical Warning

GUI automation is fragile and NOT recommended for production workflows.

Use GUI automation only when: 1. API-based methods unavailable (rare in ras-commander) 2. Testing GUI-specific features 3. Debugging HEC-RAS GUI issues

Prefer API-based methods (RasCmdr.compute_plan(), etc.) for: - Reliability (no GUI dependencies) - Speed (no window manipulation overhead) - Headless execution (servers, CI/CD) - Maintainability (less brittle than GUI automation)

Alternative: API-Based Automation

For most tasks, use ras-commander's API instead:

Python
# ❌ GUI Automation (fragile)
# Use pywinauto to click "Run" button, wait for completion

# ✅ API-Based (robust)
from ras_commander import RasCmdr, init_ras_project

init_ras_project(project_folder, "6.6")
RasCmdr.compute_plan("01")  # No GUI required!

Parameters

Configure these values to customize the notebook for your project.

Python
# =============================================================================
# PARAMETERS - Edit these to customize the notebook
# =============================================================================
from pathlib import Path

# Project Configuration
PROJECT_NAME = "Balde Eagle Creek"           # Example project to extract
RAS_VERSION = "7.0"               # HEC-RAS version (6.3, 6.5, 6.6, etc.)

print(f"Project: {PROJECT_NAME}")
print(f"HEC-RAS Version: {RAS_VERSION}")
Python
# =============================================================================
# PROJECT SETUP
# =============================================================================
import os
import sys
from pathlib import Path

# HEC-RAS version to use

# Extract example project for this notebook
project_path = RasExamples.extract_project(PROJECT_NAME, suffix="16")
init_ras_project(project_path, RAS_VERSION)

# Construct HEC-RAS executable path from version
ras_exe = fr'"C:\Program Files (x86)\HEC\HEC-RAS\{RAS_VERSION}\Ras.exe"'

print(f"Project: {project_path}")
print(f"HEC-RAS: {ras_exe}")
Python
# =============================================================================
# LAUNCH HEC-RAS (GUI Required)
# =============================================================================
# NOTE: This cell opens the HEC-RAS GUI window. Skip this cell and subsequent
# GUI automation cells if running in automated testing mode.
# =============================================================================

import subprocess
import sys

# Print instructions to the user
print("\n" + "="*60)
print("MANUAL STEP REQUIRED: Update .rasmap to Version 6.x")
print("This project was created in HEC-RAS 5.0.7. To generate stored maps in HEC-RAS 6.x, you must:")
print("1. HEC-RAS will now be opened with this project.")
print("2. In HEC-RAS, open RAS Mapper (from the main toolbar).")
print("3. When prompted, allow RAS Mapper to update the .rasmap file to the new version.")
print("4. Once the update is complete, close RAS Mapper and exit HEC-RAS.")
print("\nAfter closing HEC-RAS, return here and continue running the notebook.")
print("="*60 + "\n")

# Use ras_exe from cell 2 and project file from initialized ras object
prj_path = f'"{str(ras.prj_file)}"'

command = f"{ras_exe} {prj_path}"
print(f"Command: {command}")

# Capture the process object so we can get its PID
if sys.platform == "win32":
    hecras_process = subprocess.Popen(command)
else:
    hecras_process = subprocess.Popen([ras_exe.strip('"'), prj_path.strip('"')])

# Store the process ID for use in the next cell
hecras_pid = hecras_process.pid
print(f"Opened HEC-RAS with Process ID: {hecras_pid}")
print("Please wait for the next cell to automate RAS Mapper...")
Python
!pip install pywinauto
Python
# This cell will inspect the HEC-RAS window and list all interactable elements.
# It also provides an example of how to use the pywinauto library for more robust automation,
# including interaction with 64-bit processes like RAS Mapper.

import win32gui
import win32con
import win32api
import win32process
import time
import ctypes
from ctypes import wintypes

# ==============================================================================
# Part 1: Inspecting the HEC-RAS Window with pywin32
# ==============================================================================
# This section uses the original pywin32 approach to list all menus and
# child controls (buttons, text boxes, etc.) of the main HEC-RAS window.

# Constants from original script
MF_BYPOSITION = 0x00000400

def get_windows_by_pid(pid):
    """Find all windows belonging to a specific process ID"""
    def callback(hwnd, hwnds):
        if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd):
            _, window_pid = win32process.GetWindowThreadProcessId(hwnd)
            if window_pid == pid:
                window_title = win32gui.GetWindowText(hwnd)
                if window_title:
                    hwnds.append((hwnd, window_title))
        return True
    hwnds = []
    win32gui.EnumWindows(callback, hwnds)
    return hwnds

def find_main_hecras_window(windows):
    """Find the main HEC-RAS window from a list of windows"""
    for hwnd, title in windows:
        if "HEC-RAS" in title and win32gui.GetMenu(hwnd):
            return hwnd, title
    return None, None

def get_menu_string(menu_handle, pos):
    """Get menu item string at position"""
    buf_size = 256
    buf = ctypes.create_unicode_buffer(buf_size)
    user32 = ctypes.windll.user32
    result = user32.GetMenuStringW(menu_handle, pos, buf, buf_size, MF_BYPOSITION)
    if result:
        return buf.value
    return ""

def enumerate_all_menus(hwnd):
    """Enumerate all menus and their items in great detail."""
    menu_bar = win32gui.GetMenu(hwnd)
    if not menu_bar:
        print("No menu bar found on the window.")
        return

    menu_count = win32gui.GetMenuItemCount(menu_bar)
    print(f"\n--- Enumerating Menus ({menu_count} top-level) ---")

    for i in range(menu_count):
        menu_text = get_menu_string(menu_bar, i).replace('&', '')
        submenu = win32gui.GetSubMenu(menu_bar, i)
        print(f"\nMenu {i}: '{menu_text}'")

        if submenu:
            item_count = win32gui.GetMenuItemCount(submenu)
            print(f"  Contains {item_count} items:")
            for j in range(item_count):
                item_text = get_menu_string(submenu, j)
                menu_id = win32gui.GetMenuItemID(submenu, j)

                id_str = f"(ID: {menu_id})" if menu_id != -1 and menu_id != 0 else ""

                sub_submenu = win32gui.GetSubMenu(submenu, j)
                if sub_submenu:
                    print(f"    Item {j}: '{item_text}' -> [Submenu]")
                    sub_item_count = win32gui.GetMenuItemCount(sub_submenu)
                    for k in range(sub_item_count):
                        sub_item_text = get_menu_string(sub_submenu, k)
                        sub_menu_id = win32gui.GetMenuItemID(sub_submenu, k)
                        sub_id_str = f"(ID: {sub_menu_id})" if sub_menu_id != -1 and sub_menu_id != 0 else ""
                        print(f"      - '{sub_item_text}' {sub_id_str}")
                else:
                    print(f"    Item {j}: '{item_text}' {id_str}")
        else:
            print("  (This top-level item is not a menu)")

def enumerate_child_controls(hwnd):
    """Enumerates all child controls (widgets) of a window."""
    child_windows = []
    def callback(child_hwnd, _):
        child_windows.append(child_hwnd)
        return True

    win32gui.EnumChildWindows(hwnd, callback, None)

    print(f"\n--- Enumerating Child Controls ({len(child_windows)} found) ---")
    if not child_windows:
        print("No child controls found.")
        return

    for i, child_hwnd in enumerate(child_windows):
        class_name = win32gui.GetClassName(child_hwnd)
        window_text = win32gui.GetWindowText(child_hwnd)
        control_id = win32gui.GetDlgCtrlID(child_hwnd)

        style = win32gui.GetWindowLong(child_hwnd, win32con.GWL_STYLE)
        is_visible = (style & win32con.WS_VISIBLE) != 0

        rect = win32gui.GetWindowRect(child_hwnd)

        print(f"\nControl {i}:")
        print(f"  - HWND:        {child_hwnd}")
        print(f"  - Class Name:  '{class_name}'")
        print(f"  - Text/Caption: '{window_text}'")
        print(f"  - Control ID:  {control_id}")
        print(f"  - Visible:     {is_visible}")
        print(f"  - Position:    (L: {rect[0]}, T: {rect[1]}, R: {rect[2]}, B: {rect[3]})")

# Main execution for pywin32 inspection
if 'hecras_pid' not in globals() or hecras_pid is None:
    print("ERROR: HEC-RAS process ID not found. Please run the previous cell to launch HEC-RAS first.")
else:
    print(f"Looking for HEC-RAS windows for process ID: {hecras_pid}")
    time.sleep(2)

    windows = get_windows_by_pid(hecras_pid)
    if not windows:
        print(f"Could not find any windows for process ID {hecras_pid}")
    else:
        hec_ras_hwnd, title = find_main_hecras_window(windows)
        if not hec_ras_hwnd:
            print("Could not identify the main HEC-RAS window from the found windows:")
            for hwnd, title in windows:
                print(f"  - {title} (HWND: {hwnd})")
        else:
            print(f"\nFound main HEC-RAS window: '{title}' (HWND: {hec_ras_hwnd})")
            print("="*60)

            enumerate_all_menus(hec_ras_hwnd)
            enumerate_child_controls(hec_ras_hwnd)

            print("\n" + "="*60)
            print("Inspection complete. The lists above show all menus and controls discoverable with pywin32.")
Python
# ==============================================================================
# Part 1b: Deeper Menu and Window Object Enumeration using ctypes
# ==============================================================================

import win32gui
import win32con
import win32api
import win32process
import ctypes
from ctypes import wintypes

# Define MENUITEMINFO structure using ctypes
class MENUITEMINFO(ctypes.Structure):
    _fields_ = [
        ("cbSize", wintypes.UINT),
        ("fMask", wintypes.UINT),
        ("fType", wintypes.UINT),
        ("fState", wintypes.UINT),
        ("wID", wintypes.UINT),
        ("hSubMenu", wintypes.HMENU),
        ("hbmpChecked", wintypes.HBITMAP),
        ("hbmpUnchecked", wintypes.HBITMAP),
        ("dwItemData", ctypes.POINTER(ctypes.c_ulong)),
        ("dwTypeData", wintypes.LPWSTR),
        ("cch", wintypes.UINT),
        ("hbmpItem", wintypes.HBITMAP)
    ]

def get_menu_string(menu_handle, pos):
    """Get menu item string at position"""
    buf_size = 256
    buf = ctypes.create_unicode_buffer(buf_size)
    user32 = ctypes.windll.user32
    result = user32.GetMenuStringW(menu_handle, pos, buf, buf_size, MF_BYPOSITION)
    if result:
        return buf.value
    return ""

def enumerate_menu_item_details(menu_handle, item_index):
    """Get detailed information about a menu item using ctypes"""
    # Create and initialize MENUITEMINFO structure
    mii = MENUITEMINFO()
    mii.cbSize = ctypes.sizeof(MENUITEMINFO)
    mii.fMask = win32con.MIIM_STATE | win32con.MIIM_ID | win32con.MIIM_TYPE | win32con.MIIM_SUBMENU

    # Call GetMenuItemInfo using ctypes
    user32 = ctypes.windll.user32
    result = user32.GetMenuItemInfoW(
        menu_handle, 
        item_index, 
        True,  # fByPosition
        ctypes.byref(mii)
    )

    if result:
        # Parse state flags
        state_flags = []
        if mii.fState & win32con.MFS_CHECKED:
            state_flags.append("CHECKED")
        if mii.fState & win32con.MFS_DISABLED:
            state_flags.append("DISABLED")
        if mii.fState & win32con.MFS_GRAYED:
            state_flags.append("GRAYED")
        if mii.fState & win32con.MFS_HILITE:
            state_flags.append("HIGHLIGHTED")
        if mii.fState & win32con.MFS_DEFAULT:
            state_flags.append("DEFAULT")

        # Parse type flags
        type_flags = []
        if mii.fType & win32con.MFT_STRING:
            type_flags.append("STRING")
        if mii.fType & win32con.MFT_SEPARATOR:
            type_flags.append("SEPARATOR")
        if mii.fType & win32con.MFT_BITMAP:
            type_flags.append("BITMAP")
        if mii.fType & win32con.MFT_OWNERDRAW:
            type_flags.append("OWNERDRAW")

        return {
            "id": mii.wID,
            "type_flags": type_flags,
            "state": state_flags,
            "text": get_menu_string(menu_handle, item_index),
            "has_submenu": bool(mii.hSubMenu)
        }
    else:
        # Fallback to simpler approach if GetMenuItemInfo fails
        menu_id = win32gui.GetMenuItemID(menu_handle, item_index)
        menu_state = win32gui.GetMenuState(menu_handle, item_index, win32con.MF_BYPOSITION)

        state_flags = []
        if menu_state & win32con.MF_CHECKED:
            state_flags.append("CHECKED")
        if menu_state & win32con.MF_DISABLED:
            state_flags.append("DISABLED")
        if menu_state & win32con.MF_GRAYED:
            state_flags.append("GRAYED")
        if menu_state & win32con.MF_SEPARATOR:
            state_flags.append("SEPARATOR")

        return {
            "id": menu_id if menu_id != -1 else None,
            "state": state_flags,
            "text": get_menu_string(menu_handle, item_index),
            "has_submenu": win32gui.GetSubMenu(menu_handle, item_index) is not None,
            "fallback": True
        }

def enumerate_window_details(hwnd, indent=0):
    """Recursively enumerate window details including styles and extended styles"""
    if not hwnd:
        return

    # Get basic window info
    class_name = win32gui.GetClassName(hwnd)
    window_text = win32gui.GetWindowText(hwnd)

    # Get window styles
    style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
    ex_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)

    # Get window metrics
    rect = win32gui.GetWindowRect(hwnd)

    # Print window details
    indent_str = "  " * indent
    print(f"{indent_str}Window Handle: {hwnd}")
    print(f"{indent_str}Class: {class_name}")
    print(f"{indent_str}Text: {window_text}")
    print(f"{indent_str}Position: {rect}")
    print(f"{indent_str}Style: 0x{style:08X}")
    print(f"{indent_str}Extended Style: 0x{ex_style:08X}")

    # Enumerate child windows recursively (limit depth to avoid too much output)
    if indent < 3:  # Limit recursion depth
        child_windows = []
        win32gui.EnumChildWindows(
            hwnd,
            lambda child_hwnd, windows: windows.append(child_hwnd) or True,
            child_windows
        )

        if child_windows:
            print(f"{indent_str}Children: {len(child_windows)} found")
            for child_hwnd in child_windows[:5]:  # Show first 5 children
                print(f"{indent_str}---")
                enumerate_window_details(child_hwnd, indent + 1)
            if len(child_windows) > 5:
                print(f"{indent_str}... and {len(child_windows) - 5} more children")

def enumerate_full_menu_tree(hwnd):
    """Enumerate complete menu tree with all available details"""
    menu_bar = win32gui.GetMenu(hwnd)
    if not menu_bar:
        print("No menu bar found")
        return

    print("\n=== Complete Menu Tree Analysis ===\n")

    menu_count = win32gui.GetMenuItemCount(menu_bar)
    for i in range(menu_count):
        menu_text = get_menu_string(menu_bar, i)
        menu_details = enumerate_menu_item_details(menu_bar, i)
        print(f"\nTop Level Menu {i}: {menu_text}")
        print(f"Details: {menu_details}")

        submenu = win32gui.GetSubMenu(menu_bar, i)
        if submenu:
            submenu_count = win32gui.GetMenuItemCount(submenu)
            print(f"Contains {submenu_count} items:")

            for j in range(min(submenu_count, 10)):  # Show first 10 items
                submenu_text = get_menu_string(submenu, j)
                submenu_details = enumerate_menu_item_details(submenu, j)
                print(f"  └─ Item {j}: {submenu_text}")
                print(f"     Details: {submenu_details}")

                # Check for sub-submenus
                sub_submenu = win32gui.GetSubMenu(submenu, j)
                if sub_submenu:
                    sub_count = win32gui.GetMenuItemCount(sub_submenu)
                    print(f"     Has submenu with {sub_count} items:")
                    for k in range(min(sub_count, 5)):  # Show first 5 sub-items
                        sub_text = get_menu_string(sub_submenu, k)
                        sub_details = enumerate_menu_item_details(sub_submenu, k)
                        print(f"       └─ Sub-item {k}: {sub_text}")
                        print(f"          Details: {sub_details}")

            if submenu_count > 10:
                print(f"  ... and {submenu_count - 10} more items")

# Main execution
if 'hecras_pid' in globals() and hecras_pid is not None:
    # Wait for windows to be ready
    import time
    time.sleep(1)

    # Find HEC-RAS windows
    windows = get_windows_by_pid(hecras_pid)
    hec_ras_hwnd, title = find_main_hecras_window(windows)

    if hec_ras_hwnd:
        print("\n" + "="*80)
        print("Performing detailed menu enumeration...")
        enumerate_full_menu_tree(hec_ras_hwnd)

        print("\n" + "="*80)
        print("Performing detailed window hierarchy enumeration...")
        print(f"Main window: {title}")
        enumerate_window_details(hec_ras_hwnd)

        print("\n" + "="*80)
        print("Detailed enumeration complete.")
    else:
        print("Could not find main HEC-RAS window")
else:
    print("HEC-RAS process ID not found. Please run the cell that launches HEC-RAS first.")
Python
def enumerate_menu_item_details(menu_handle, item_index):
    """Get detailed information about a menu item"""
    # Get menu item ID
    menu_id = win32gui.GetMenuItemID(menu_handle, item_index)

    # Get menu state
    menu_state = win32gui.GetMenuState(menu_handle, item_index, win32con.MF_BYPOSITION)

    # Parse state flags
    state_flags = []
    if menu_state & win32con.MF_CHECKED:
        state_flags.append("CHECKED")
    if menu_state & win32con.MF_DISABLED:
        state_flags.append("DISABLED")
    if menu_state & win32con.MF_GRAYED:
        state_flags.append("GRAYED")
    if menu_state & win32con.MF_SEPARATOR:
        state_flags.append("SEPARATOR")

    # Get menu text
    text = get_menu_string(menu_handle, item_index)

    return {
        "id": menu_id if menu_id != -1 else None,
        "state": state_flags,
        "text": text,
        "has_submenu": win32gui.GetSubMenu(menu_handle, item_index) is not None
    }
Python
# ==============================================================================
# Part 2: Interacting with 64-bit Processes using pywinauto
# ==============================================================================
print("\n--- pywinauto Example ---")
print("This section shows how to inspect an application like RAS Mapper using pywinauto.")
print("You may need to install it first: !pip install pywinauto")

# Handle optional dependency with specific ImportError
try:
    import pywinauto
    from pywinauto import timings, findwindows
except ImportError:
    print("\npywinauto is not installed. Please install it to run this example:")
    print("In a new cell, run: !pip install pywinauto")
    raise

print("pywinauto is installed.")

print("\nAttempting to find a RAS Mapper window to demonstrate pywinauto...")

# Handle expected case where RAS Mapper may not be running
try:
    # Connect to RAS Mapper window (title might vary slightly by version)
    # Using win32 backend as RAS Mapper is a mix of technologies
    app = pywinauto.Application(backend="win32").connect(title_re=".*RAS Mapper.*", timeout=5)

    ras_mapper_window = app.window(title_re=".*RAS Mapper.*")

    print("\nSuccessfully connected to RAS Mapper!")
    print("Now, listing all its controls using pywinauto's print_control_identifiers():")

    # This will print a tree of all interactable controls.
    # It's a very useful function for exploration.
    ras_mapper_window.print_control_identifiers()

except (findwindows.ElementNotFoundError, timings.TimeoutError):
    # This is expected when RAS Mapper is not running - provide guidance
    print("\nCould not find a running RAS Mapper window.")
    print("To run this for real, open HEC-RAS and then RAS Mapper, then execute this cell again.")
    print("Example code to list controls once connected:")
    print("  from pywinauto import Application")
    print("  app = Application(backend='uia').connect(title_re='.*RAS Mapper.*') # uia backend might also work")
    print("  ras_mapper_window = app.window(title_re='.*RAS Mapper.*')")
    print("  ras_mapper_window.print_control_identifiers()")

RasGuiAutomation Library Functions

The exploration above shows the low-level Win32 API techniques used to automate HEC-RAS. The ras-commander library encapsulates these patterns in the RasGuiAutomation class, providing high-level functions for common automation tasks:

Available Functions

Function Purpose
open_rasmapper() Opens RASMapper via GIS Tools menu, waits for user
run_unsteady_gui() Opens Unsteady Flow Analysis dialog and starts computation
handle_already_running_dialog() Dismisses "already running" dialog when launching HEC-RAS

Dialog Handling

When automating HEC-RAS, a common challenge is handling the "already running" dialog that appears when launching HEC-RAS while another instance is open. The library provides handle_already_running_dialog() to automatically dismiss this dialog.

Dialog Detection Approach

The function uses these Win32 techniques (as explored above): 1. Window enumeration - Find dialogs with class #32770 (standard Windows dialog) 2. Text matching - Check for keywords like "already" or "running" in the dialog text 3. Button clicking - Send WM_COMMAND to click "Yes" button

Python
# =============================================================================
# Example: Using RasGuiAutomation Library Functions
# =============================================================================
# Instead of writing all the Win32 code manually (as shown above), use the
# library functions for common automation tasks.

from ras_commander import RasGuiAutomation

# Example 1: Handle "already running" dialog
# ------------------------------------------
# Call this shortly after launching HEC-RAS to dismiss the dialog if it appears.
# Returns True if dialog was found and dismissed, False otherwise.
#
# dialog_found = RasGuiAutomation.handle_already_running_dialog(timeout=5)
# if dialog_found:
#     print("Dismissed 'already running' dialog")
# else:
#     print("No dialog appeared (HEC-RAS started normally)")

# Example 2: Open RASMapper
# -------------------------
# Opens HEC-RAS with the current project, navigates to GIS Tools > RAS Mapper,
# and waits for user to close RASMapper. Useful for .rasmap upgrades.
#
# success = RasGuiAutomation.open_rasmapper(wait_for_user=True, timeout=300)
# if success:
#     print("RASMapper closed successfully")

# Example 3: Run Unsteady Flow Analysis via GUI
# ---------------------------------------------
# Opens the Unsteady Flow Analysis dialog and starts computation.
# Used internally by RasMap.postprocess_stored_maps() for floodplain mapping.
#
# success = RasGuiAutomation.run_unsteady_gui(plan_number="06", wait_for_user=True)
# if success:
#     print("Computation completed")

print("RasGuiAutomation functions available:")
print("  - handle_already_running_dialog(timeout=5)")
print("  - open_rasmapper(wait_for_user=True, timeout=300)")
print("  - run_unsteady_gui(plan_number, wait_for_user=True)")
print("\nSee examples/15_stored_map_generation.ipynb for real usage.")
Python
# ==============================================================================
# Part 1b: Deeper Menu and Window Object Enumeration using ctypes
# ==============================================================================

import win32gui
import win32con
import win32api
import win32process
import ctypes
from ctypes import wintypes

# Define MENUITEMINFO structure using ctypes
class MENUITEMINFO(ctypes.Structure):
    _fields_ = [
        ("cbSize", wintypes.UINT),
        ("fMask", wintypes.UINT),
        ("fType", wintypes.UINT),
        ("fState", wintypes.UINT),
        ("wID", wintypes.UINT),
        ("hSubMenu", wintypes.HMENU),
        ("hbmpChecked", wintypes.HBITMAP),
        ("hbmpUnchecked", wintypes.HBITMAP),
        ("dwItemData", ctypes.POINTER(ctypes.c_ulong)),
        ("dwTypeData", wintypes.LPWSTR),
        ("cch", wintypes.UINT),
        ("hbmpItem", wintypes.HBITMAP)
    ]

def get_menu_string(menu_handle, pos):
    """Get menu item string at position"""
    buf_size = 256
    buf = ctypes.create_unicode_buffer(buf_size)
    user32 = ctypes.windll.user32
    result = user32.GetMenuStringW(menu_handle, pos, buf, buf_size, MF_BYPOSITION)
    if result:
        return buf.value
    return ""

def enumerate_menu_item_details(menu_handle, item_index):
    """Get detailed information about a menu item using ctypes"""
    # Create and initialize MENUITEMINFO structure
    mii = MENUITEMINFO()
    mii.cbSize = ctypes.sizeof(MENUITEMINFO)
    mii.fMask = win32con.MIIM_STATE | win32con.MIIM_ID | win32con.MIIM_TYPE | win32con.MIIM_SUBMENU

    # Call GetMenuItemInfo using ctypes
    user32 = ctypes.windll.user32
    result = user32.GetMenuItemInfoW(
        menu_handle, 
        item_index, 
        True,  # fByPosition
        ctypes.byref(mii)
    )

    if result:
        # Parse state flags
        state_flags = []
        if mii.fState & win32con.MFS_CHECKED:
            state_flags.append("CHECKED")
        if mii.fState & win32con.MFS_DISABLED:
            state_flags.append("DISABLED")
        if mii.fState & win32con.MFS_GRAYED:
            state_flags.append("GRAYED")
        if mii.fState & win32con.MFS_HILITE:
            state_flags.append("HIGHLIGHTED")
        if mii.fState & win32con.MFS_DEFAULT:
            state_flags.append("DEFAULT")

        # Parse type flags
        type_flags = []
        if mii.fType & win32con.MFT_STRING:
            type_flags.append("STRING")
        if mii.fType & win32con.MFT_SEPARATOR:
            type_flags.append("SEPARATOR")
        if mii.fType & win32con.MFT_BITMAP:
            type_flags.append("BITMAP")
        if mii.fType & win32con.MFT_OWNERDRAW:
            type_flags.append("OWNERDRAW")

        return {
            "id": mii.wID,
            "type_flags": type_flags,
            "state": state_flags,
            "text": get_menu_string(menu_handle, item_index),
            "has_submenu": bool(mii.hSubMenu)
        }
    else:
        # Fallback to simpler approach if GetMenuItemInfo fails
        menu_id = win32gui.GetMenuItemID(menu_handle, item_index)
        menu_state = win32gui.GetMenuState(menu_handle, item_index, win32con.MF_BYPOSITION)

        state_flags = []
        if menu_state & win32con.MF_CHECKED:
            state_flags.append("CHECKED")
        if menu_state & win32con.MF_DISABLED:
            state_flags.append("DISABLED")
        if menu_state & win32con.MF_GRAYED:
            state_flags.append("GRAYED")
        if menu_state & win32con.MF_SEPARATOR:
            state_flags.append("SEPARATOR")

        return {
            "id": menu_id if menu_id != -1 else None,
            "state": state_flags,
            "text": get_menu_string(menu_handle, item_index),
            "has_submenu": win32gui.GetSubMenu(menu_handle, item_index) is not None,
            "fallback": True
        }

def enumerate_window_details(hwnd, indent=0):
    """Recursively enumerate window details including styles and extended styles"""
    if not hwnd:
        return

    # Get basic window info
    class_name = win32gui.GetClassName(hwnd)
    window_text = win32gui.GetWindowText(hwnd)

    # Get window styles
    style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
    ex_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)

    # Get window metrics
    rect = win32gui.GetWindowRect(hwnd)

    # Print window details
    indent_str = "  " * indent
    print(f"{indent_str}Window Handle: {hwnd}")
    print(f"{indent_str}Class: {class_name}")
    print(f"{indent_str}Text: {window_text}")
    print(f"{indent_str}Position: {rect}")
    print(f"{indent_str}Style: 0x{style:08X}")
    print(f"{indent_str}Extended Style: 0x{ex_style:08X}")

    # Enumerate child windows recursively (limit depth to avoid too much output)
    if indent < 3:  # Limit recursion depth
        child_windows = []
        win32gui.EnumChildWindows(
            hwnd,
            lambda child_hwnd, windows: windows.append(child_hwnd) or True,
            child_windows
        )

        if child_windows:
            print(f"{indent_str}Children: {len(child_windows)} found")
            for child_hwnd in child_windows[:5]:  # Show first 5 children
                print(f"{indent_str}---")
                enumerate_window_details(child_hwnd, indent + 1)
            if len(child_windows) > 5:
                print(f"{indent_str}... and {len(child_windows) - 5} more children")

def enumerate_full_menu_tree(hwnd):
    """Enumerate complete menu tree with all available details"""
    menu_bar = win32gui.GetMenu(hwnd)
    if not menu_bar:
        print("No menu bar found")
        return

    print("\n=== Complete Menu Tree Analysis ===\n")

    menu_count = win32gui.GetMenuItemCount(menu_bar)
    for i in range(menu_count):
        menu_text = get_menu_string(menu_bar, i)
        menu_details = enumerate_menu_item_details(menu_bar, i)
        print(f"\nTop Level Menu {i}: {menu_text}")
        print(f"Details: {menu_details}")

        submenu = win32gui.GetSubMenu(menu_bar, i)
        if submenu:
            submenu_count = win32gui.GetMenuItemCount(submenu)
            print(f"Contains {submenu_count} items:")

            for j in range(min(submenu_count, 10)):  # Show first 10 items
                submenu_text = get_menu_string(submenu, j)
                submenu_details = enumerate_menu_item_details(submenu, j)
                print(f"  └─ Item {j}: {submenu_text}")
                print(f"     Details: {submenu_details}")

                # Check for sub-submenus
                sub_submenu = win32gui.GetSubMenu(submenu, j)
                if sub_submenu:
                    sub_count = win32gui.GetMenuItemCount(sub_submenu)
                    print(f"     Has submenu with {sub_count} items:")
                    for k in range(min(sub_count, 5)):  # Show first 5 sub-items
                        sub_text = get_menu_string(sub_submenu, k)
                        sub_details = enumerate_menu_item_details(sub_submenu, k)
                        print(f"       └─ Sub-item {k}: {sub_text}")
                        print(f"          Details: {sub_details}")

            if submenu_count > 10:
                print(f"  ... and {submenu_count - 10} more items")

# Main execution
if 'hecras_pid' in globals() and hecras_pid is not None:
    # Wait for windows to be ready
    import time
    time.sleep(1)

    # Find HEC-RAS windows
    windows = get_windows_by_pid(hecras_pid)
    hec_ras_hwnd, title = find_main_hecras_window(windows)

    if hec_ras_hwnd:
        print("\n" + "="*80)
        print("Performing detailed menu enumeration...")
        enumerate_full_menu_tree(hec_ras_hwnd)

        print("\n" + "="*80)
        print("Performing detailed window hierarchy enumeration...")
        print(f"Main window: {title}")
        enumerate_window_details(hec_ras_hwnd)

        print("\n" + "="*80)
        print("Detailed enumeration complete.")
    else:
        print("Could not find main HEC-RAS window")
else:
    print("HEC-RAS process ID not found. Please run the cell that launches HEC-RAS first.")

GUI Automation Best Practices

Prefer API Over GUI

Decision Tree:

Text Only
Need to automate HEC-RAS task?
  ├─ Is there a ras-commander API method?
  │  ├─ YES → Use API (RasCmdr, RasPlan, etc.)
  │  └─ NO → Continue...
  ├─ Is there a COM interface method? (HEC-RAS 5.x)
  │  ├─ YES → Use RasControl class
  │  └─ NO → Continue...
  └─ Must use GUI automation (last resort)
     ├─ Use RasGuiAutomation helpers
     └─ Document why API not available

Making GUI Automation Robust

Pattern 1: Retry Logic

Python
def click_with_retry(element, max_attempts=3):
    for attempt in range(max_attempts):
        try:
            element.click()
            return True
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            time.sleep(1)

    return False

Pattern 2: Graceful Degradation

Python
try:
    # Attempt GUI automation
    result = automate_via_gui()
except Exception as e:
    # Fall back to manual instructions
    print(f"GUI automation failed: {e}")
    print("Please complete this step manually:")
    print("  1. Open HEC-RAS")
    print("  2. Click File → Export → Google Earth")
    print("  3. Save to output folder")
    input("Press Enter when complete...")

Pattern 3: Error Recovery

Python
def safe_gui_automation(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            # Log error
            print(f"GUI automation error: {e}")

            # Try to recover GUI state
            try:
                app = Application().connect(title_re=".*HEC-RAS.*")
                app.window().type_keys("{ESC}")  # Close any dialogs
                app.window().type_keys("{ESC}")
            except:
                pass

            raise

    return wrapper

LLM Forward: Document Manual Steps

When GUI automation is required, document for manual review:

Python
def document_gui_steps(task_description, steps, output_file):
    import json
    from datetime import datetime

    documentation = {
        'timestamp': datetime.now().isoformat(),
        'task': task_description,
        'automation_type': 'GUI (fragile - consider API alternative)',
        'manual_steps': steps,
        'review_instructions': [
            '1. Verify all steps completed successfully',
            '2. Check output files exist',
            '3. Open HEC-RAS GUI to confirm results',
            '4. Document any errors encountered'
        ]
    }

    with open(output_file, 'w') as f:
        json.dump(documentation, f, indent=2)

    print(f"GUI automation documented: {output_file}")

# Usage - save to project folder
document_gui_steps(
    task_description="Export cross sections to CSV",
    steps=[
        "Open HEC-RAS project",
        "Navigate to Geometry → Cross Sections",
        "Click File → Export → CSV",
        "Select output folder",
        "Click Save"
    ],
    output_file=project_path / 'gui_automation_log.json'
)

Resources

  • pywinauto documentation: https://pywinauto.readthedocs.io/
  • win32gui examples: Search for "win32gui python examples"

Future Direction

ras-commander aims to provide API coverage for all common tasks, eliminating need for GUI automation. If you find yourself using GUI automation frequently, please file a GitHub issue requesting API support for that functionality.

This provides the basic building blocks for GUI-driven automations for HEC-RAS without the HECRASController

CLB Engineering Corporation  ·  LLM Forward Engineering
RAS Commander is a free and open-source project maintained by CLB Engineering Corporation. For agencies and firms seeking to modernize H&H workflows with LLM Forward approaches, contact CLB to partner with the engineers who wrote the automation.