Win32COM Automation¶
# =============================================================================
# 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:
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:
-
Window Not Found: HEC-RAS not running or title changed
Pythonimport 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) -
Timing Issues: GUI not ready for next action
-
Element Not Found: Control identifiers changed
Prerequisites¶
Before running this notebook, ensure you have:
- ras-commander installed:
pip install ras-commander - Python 3.10+: Check with
python --version - HEC-RAS installed: GUI automation requires running application
- pywin32:
pip install pywin32 - pywinauto:
pip install pywinauto(for advanced automation) - 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
Related Notebooks¶
- 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:
# ❌ 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.
# =============================================================================
# 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}")
# =============================================================================
# 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}")
# =============================================================================
# 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...")
# 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.")
# ==============================================================================
# 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.")
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
}
# ==============================================================================
# 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
# =============================================================================
# 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.")
# ==============================================================================
# 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:
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
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
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
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:
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.