use std::collections::HashMap;
use uiautomation::types::ControlType;
use uiautomation::{Result, UIAutomation, UIElement, UITreeWalker};
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect(); let mode = args.first().map(String::as_str).unwrap_or("help");
let auto = UIAutomation::new().expect("CoInitialize/IUIAutomation init");
let result = match mode {
"windows" => list_windows(&auto),
"focused" => show_focused(&auto),
"fginfo" => {
fginfo::run();
Ok(())
}
"tree" => dump_tree(&auto, args.get(1).map(String::as_str), args.get(2)),
"ic" => item_container(
&auto,
args.get(1).map(String::as_str),
args.get(2).map(String::as_str),
),
_ => {
eprintln!(
"modes: windows | focused | tree <pid|name-substr> [max_depth] | ic <window-substr> <name>"
);
Ok(())
}
};
if let Err(e) = result {
eprintln!("✗ {mode} failed: {e:?}");
std::process::exit(1);
}
}
fn list_windows(auto: &UIAutomation) -> Result<()> {
let walker = auto.create_tree_walker()?;
let root = auto.get_root_element()?;
let mut total = 0;
let mut unreadable = 0;
let mut child = walker.get_first_child(&root).ok();
while let Some(w) = child {
let pid = w.get_process_id().unwrap_or_default();
let class = w.get_classname().unwrap_or_default();
let name = match w.get_name() {
Ok(n) if !n.is_empty() => n,
Ok(_) => "(empty name)".to_string(),
Err(_) => {
unreadable += 1;
"«UNREADABLE — UIPI/权限墙?»".to_string()
}
};
println!("pid={pid:<7} class={class:<28} {name}");
total += 1;
child = walker.get_next_sibling(&w).ok();
}
println!("--- windows={total} unreadable={unreadable}(后者多为更高完整性级别进程) ---");
Ok(())
}
fn show_focused(auto: &UIAutomation) -> Result<()> {
let f = auto.get_focused_element()?;
println!("name = {:?}", f.get_name());
println!("class = {:?}", f.get_classname());
println!("control_type = {:?}", f.get_control_type());
println!("automation_id = {:?}", f.get_automation_id());
println!("runtime_id = {:?}", f.get_runtime_id());
println!("process_id = {}", f.get_process_id()?);
println!("offscreen = {:?}", f.is_offscreen());
Ok(())
}
struct Stats {
total: usize,
interactive: usize,
empty_name: usize,
max_seen_depth: usize,
by_type: HashMap<String, usize>,
}
fn dump_tree(auto: &UIAutomation, target: Option<&str>, max_depth: Option<&String>) -> Result<()> {
let Some(target) = target else {
eprintln!("tree 需要目标:pid 数字或名称子串");
return Ok(());
};
let max_depth: usize = max_depth.and_then(|s| s.parse().ok()).unwrap_or(12);
let walker = auto.create_tree_walker()?;
let root = auto.get_root_element()?;
let mut child = walker.get_first_child(&root).ok();
let start = loop {
let Some(w) = child else {
eprintln!("未找到匹配窗口:{target}");
return Ok(());
};
let hit = match target.parse::<u32>() {
Ok(pid) => w.get_process_id().map(|p| p == pid).unwrap_or(false),
Err(_) => w.get_name().map(|n| n.contains(target)).unwrap_or(false),
};
if hit {
break w;
}
child = walker.get_next_sibling(&w).ok();
};
println!(
"target: name={:?} class={:?} pid={}",
start.get_name().unwrap_or_default(),
start.get_classname().unwrap_or_default(),
start.get_process_id().unwrap_or_default()
);
let mut stats = Stats {
total: 0,
interactive: 0,
empty_name: 0,
max_seen_depth: 0,
by_type: HashMap::new(),
};
let mut printed = 0;
walk(&walker, &start, 0, max_depth, &mut stats, &mut printed);
println!("\n=== stats(depth≤{max_depth}) ===");
println!(
"total={} interactive={} empty_name={} real_depth={}",
stats.total, stats.interactive, stats.empty_name, stats.max_seen_depth
);
let mut types: Vec<_> = stats.by_type.into_iter().collect();
types.sort_by_key(|(_, n)| std::cmp::Reverse(*n));
for (t, n) in types.iter().take(15) {
println!(" {t:<18} ×{n}");
}
Ok(())
}
const INTERACTIVE: &[&str] = &[
"Button",
"CheckBox",
"RadioButton",
"ComboBox",
"Edit",
"ListItem",
"MenuItem",
"TabItem",
"Hyperlink",
"ToggleButton",
"DataGrid",
"Spinner",
"Slider",
"Document",
];
fn walk(
walker: &UITreeWalker,
elem: &UIElement,
depth: usize,
max_depth: usize,
stats: &mut Stats,
printed: &mut usize,
) {
stats.total += 1;
stats.max_seen_depth = stats.max_seen_depth.max(depth);
let ty = format!(
"{:?}",
elem.get_control_type().unwrap_or(ControlType::Custom)
);
*stats.by_type.entry(ty.clone()).or_default() += 1;
let name = elem.get_name().unwrap_or_default();
if name.is_empty() {
stats.empty_name += 1;
}
if INTERACTIVE.contains(&ty.as_str()) {
stats.interactive += 1;
}
if *printed < 220 && depth <= max_depth {
let aid = elem.get_automation_id().unwrap_or_default();
let shown = if name.is_empty() {
"·".to_string()
} else {
name
};
println!("{}{}[{aid}] {shown}", " ".repeat(depth), ty);
*printed += 1;
}
if depth >= max_depth {
return;
}
let mut child = walker.get_first_child(elem).ok();
while let Some(c) = child {
walk(walker, &c, depth + 1, max_depth, stats, printed);
child = walker.get_next_sibling(&c).ok();
}
}
mod fginfo {
#![allow(unsafe_code)]
use windows::Win32::UI::WindowsAndMessaging::{
GUITHREADINFO, GetClassNameW, GetForegroundWindow, GetGUIThreadInfo, GetWindowTextW,
};
pub fn run() {
unsafe {
let fg = GetForegroundWindow();
let mut buf = [0u16; 256];
GetWindowTextW(fg, &mut buf);
println!("foreground : {}", String::from_utf16_lossy(&buf));
let mut class = [0u16; 256];
GetClassNameW(fg, &mut class);
println!("fg class : {}", String::from_utf16_lossy(&class));
let mut info = GUITHREADINFO {
cbSize: std::mem::size_of::<GUITHREADINFO>() as u32,
..Default::default()
};
if GetGUIThreadInfo(0, &mut info).is_ok() {
let mut cbuf = [0u16; 256];
let n = GetClassNameW(info.hwndFocus, &mut cbuf);
println!(
"focus hwnd : {} class={}",
info.hwndFocus.0 as usize,
String::from_utf16_lossy(&cbuf[..n.max(0) as usize])
);
} else {
println!("GetGUIThreadInfo failed");
}
}
}
}
#[allow(unsafe_code)] fn item_container(auto: &UIAutomation, window: Option<&str>, needle: Option<&str>) -> Result<()> {
use uiautomation::patterns::UIItemContainerPattern;
use uiautomation::variants::Variant;
use windows::Win32::UI::Accessibility::{
IUIAutomationItemContainerPattern, UIA_ItemContainerPatternId, UIA_NamePropertyId,
};
use windows::core::Interface;
let (Some(window), Some(needle)) = (window, needle) else {
eprintln!("ic 需要参数:<window-substr> <name-to-find>");
return Ok(());
};
let walker = auto.create_tree_walker()?;
let root = auto.get_root_element()?;
let mut child = walker.get_first_child(&root).ok();
let start = loop {
let Some(w) = child else {
eprintln!("未找到匹配窗口:{window}");
return Ok(());
};
if w.get_name().map(|n| n.contains(window)).unwrap_or(false) {
break w;
}
child = walker.get_next_sibling(&w).ok();
};
println!(
"window: {:?} class={:?} pid={}",
start.get_name().unwrap_or_default(),
start.get_classname().unwrap_or_default(),
start.get_process_id().unwrap_or_default()
);
let mut containers: Vec<UIElement> = Vec::new();
let mut realized_hits = 0usize;
let mut total = 0usize;
fn dfs(
walker: &UITreeWalker,
elem: &UIElement,
depth: usize,
needle: &str,
containers: &mut Vec<UIElement>,
realized_hits: &mut usize,
total: &mut usize,
) {
if depth > 40 || *total > 5000 {
return;
}
*total += 1;
let name = elem.get_name().unwrap_or_default();
if name.contains(needle) {
*realized_hits += 1;
}
if elem.get_pattern::<UIItemContainerPattern>().is_ok() {
containers.push(elem.clone());
}
let mut c = walker.get_first_child(elem).ok();
while let Some(x) = c {
dfs(
walker,
&x,
depth + 1,
needle,
containers,
realized_hits,
total,
);
c = walker.get_next_sibling(&x).ok();
}
}
dfs(
&walker,
&start,
0,
needle,
&mut containers,
&mut realized_hits,
&mut total,
);
println!(
"walker: total={total} containers={} realized_name_hits={realized_hits}",
containers.len()
);
for c in containers.iter().take(5) {
println!(
" container: {:?} [{:?}] class={:?}",
c.get_name().unwrap_or_default(),
c.get_control_type()
.map(|t| format!("{t:?}"))
.unwrap_or_default(),
c.get_classname().unwrap_or_default()
);
}
let value = Variant::from(needle.to_string());
for c in &containers {
let raw = c.as_ref();
let pattern: IUIAutomationItemContainerPattern =
match (|| unsafe { raw.GetCurrentPattern(UIA_ItemContainerPatternId)?.cast() })() {
Ok(p) => p,
Err(e) => {
println!(" ! GetCurrentPattern failed: {e:?}");
continue;
}
};
let hit = unsafe { pattern.FindItemByProperty(None, UIA_NamePropertyId, value.as_ref()) };
match hit.map(UIElement::from) {
Ok(elem) => {
println!(
" ✓ FindItemByProperty HIT: name={:?} role={:?} class={:?} offscreen={:?}",
elem.get_name().unwrap_or_default(),
elem.get_control_type()
.map(|t| format!("{t:?}"))
.unwrap_or_default(),
elem.get_classname().unwrap_or_default(),
elem.is_offscreen().unwrap_or(true)
);
}
Err(e) => println!(" ✗ no hit: {e:?}"),
}
}
Ok(())
}