automation实现inspect元素

This commit is contained in:
fofolee 2025-01-15 01:33:28 +08:00
parent 021cce5947
commit a3dc6479f2
11 changed files with 933 additions and 263 deletions

9
automation.cs Normal file
View File

@ -0,0 +1,9 @@
// 如果指定了过滤条件,创建一个组合条件
if (!string.IsNullOrEmpty(filter))
{
searchCondition = new OrCondition(
new PropertyCondition(AutomationElement.NameProperty, filter, PropertyConditionFlags.IgnoreCase),
new PropertyCondition(AutomationElement.ClassNameProperty, filter, PropertyConditionFlags.IgnoreCase),
new PropertyCondition(AutomationElement.AutomationIdProperty, filter, PropertyConditionFlags.IgnoreCase)
);
}

View File

@ -9,9 +9,16 @@ using System.Web.Script.Serialization;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using System.Drawing;
public class AutomationManager
{
// UIA Control Type IDs
private static class UIA_ControlTypeIds
{
public const int Window = 50032;
}
// 用于缓存找到的元素
private static Dictionary<string, AutomationElement> elementCache = new Dictionary<string, AutomationElement>();
@ -59,6 +66,15 @@ public class AutomationManager
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern bool BlockInput(bool fBlockIt);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
public static void Main(string[] args)
{
if (args.Length == 0 || args[0] == "-h" || args[0] == "--help")
@ -79,17 +95,10 @@ public class AutomationManager
switch (type.ToLower())
{
case "list":
// 列出所有元素
string filter = GetArgumentValue(args, "-filter"); // 可选的过滤条件
ListElements(args, filter);
break;
case "find":
// 查找元素
string by = GetArgumentValue(args, "-by") ?? "name";
string value = GetArgumentValue(args, "-value");
string scope = GetArgumentValue(args, "-scope") ?? "children";
FindElement(args, by, value, scope);
// 列出所有元素或查找元素
string filter = GetArgumentValue(args, "-value") ?? GetArgumentValue(args, "-filter"); // 兼容两种参数名
ListElements(args, filter);
break;
case "getinfo":
@ -135,8 +144,8 @@ public class AutomationManager
case "wait":
// 等待元素
by = GetArgumentValue(args, "-by") ?? "name";
value = GetArgumentValue(args, "-value");
string by = GetArgumentValue(args, "-by") ?? "name";
string value = GetArgumentValue(args, "-value");
int timeout = int.Parse(GetArgumentValue(args, "-timeout") ?? "30");
WaitForElement(args, by, value, timeout);
break;
@ -171,8 +180,7 @@ public class AutomationManager
case "inspect":
// 生成元素识别器
int inspectTimeout = int.Parse(GetArgumentValue(args, "-timeout") ?? "30");
InspectElement(args, inspectTimeout);
InspectElement(args);
break;
default:
@ -203,6 +211,7 @@ public class AutomationManager
- process
- active ()
-background ()
:
-value <>
@ -215,10 +224,8 @@ public class AutomationManager
:
1. inspect -
:
-timeout <> (,30)
:
automation.exe -type inspect -timeout 60
automation.exe -type inspect
:
@ -465,7 +472,9 @@ public class AutomationManager
var info = new Dictionary<string, object>();
info.Add("name", element.Current.Name);
info.Add("class", element.Current.ClassName);
info.Add("type", element.Current.ControlType.ProgrammaticName);
info.Add("controlType", element.Current.ControlType.ProgrammaticName);
info.Add("controlClass", element.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""));
info.Add("localizedType", element.Current.LocalizedControlType);
info.Add("automationId", element.Current.AutomationId);
info.Add("processId", element.Current.ProcessId);
info.Add("boundingRectangle", new
@ -486,7 +495,18 @@ public class AutomationManager
string identifier = GetArgumentValue(args, "-value");
string by = GetArgumentValue(args, "-by");
string pattern = GetArgumentValue(args, "-pattern") ?? "invoke";
bool background = HasArgument(args, "-background");
var element = GetElementByIdentifier(args, identifier, by);
// 只在非后台操作时激活窗口
if (!background)
{
var hwnd = new IntPtr(element.Current.NativeWindowHandle);
SetForegroundWindow(hwnd);
Thread.Sleep(50); // 等待窗口激活
}
switch (pattern.ToLower())
{
case "invoke":
@ -512,6 +532,11 @@ public class AutomationManager
throw new Exception("元素不支持指定的操作模式");
}
private static bool HasArgument(string[] args, string key)
{
return args.Contains(key, StringComparer.OrdinalIgnoreCase);
}
private static string[] GetSupportedPatterns(AutomationElement element)
{
var patterns = new List<string>();
@ -527,8 +552,18 @@ public class AutomationManager
{
string identifier = GetArgumentValue(args, "-value");
string by = GetArgumentValue(args, "-by");
bool background = HasArgument(args, "-background");
var element = GetElementByIdentifier(args, identifier, by);
// 只在非后台操作时激活窗口
if (!background)
{
var hwnd = new IntPtr(element.Current.NativeWindowHandle);
SetForegroundWindow(hwnd);
Thread.Sleep(50); // 等待窗口激活
}
// 尝试ValuePattern
var valuePattern = element.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
if (valuePattern != null)
@ -698,7 +733,7 @@ public class AutomationManager
{
try
{
FindElement(args, by, value, "subtree");
ListElements(args, value);
Console.WriteLine("成功找到元素");
return;
}
@ -734,15 +769,25 @@ public class AutomationManager
private static void SendKeys(string[] args, string keys)
{
string identifier = GetArgumentValue(args, "-value");
string by = GetArgumentValue(args, "-by");
bool background = HasArgument(args, "-background");
var element = GetElementByIdentifier(args, identifier, by);
// 只在非后台操作时激活窗口
if (!background)
{
var hwnd = new IntPtr(element.Current.NativeWindowHandle);
SetForegroundWindow(hwnd);
Thread.Sleep(50); // 等待窗口激活
}
if (string.IsNullOrEmpty(keys))
{
throw new Exception("必须指定要发送的按键");
}
string identifier = GetArgumentValue(args, "-value");
string by = GetArgumentValue(args, "-by");
var element = GetElementByIdentifier(args, identifier, by);
// 确保元素可以接收输入
if (!element.Current.IsKeyboardFocusable)
{
@ -824,7 +869,9 @@ public class AutomationManager
childInfo.Add("id", CacheElement(child));
childInfo.Add("name", child.Current.Name);
childInfo.Add("class", child.Current.ClassName);
childInfo.Add("type", child.Current.ControlType.ProgrammaticName);
childInfo.Add("controlType", child.Current.ControlType.ProgrammaticName);
childInfo.Add("type", child.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""));
childInfo.Add("localizedType", child.Current.LocalizedControlType);
childInfo.Add("automationId", child.Current.AutomationId);
result.Add(childInfo);
}
@ -846,7 +893,9 @@ public class AutomationManager
parentInfo.Add("id", CacheElement(parent));
parentInfo.Add("name", parent.Current.Name);
parentInfo.Add("class", parent.Current.ClassName);
parentInfo.Add("type", parent.Current.ControlType.ProgrammaticName);
parentInfo.Add("controlType", parent.Current.ControlType.ProgrammaticName);
parentInfo.Add("type", parent.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""));
parentInfo.Add("localizedType", parent.Current.LocalizedControlType);
parentInfo.Add("automationId", parent.Current.AutomationId);
var serializer = new JavaScriptSerializer();
@ -891,32 +940,131 @@ public class AutomationManager
{
var root = GetRootElement(args);
var result = new List<Dictionary<string, string>>();
string window = GetArgumentValue(args, "-window");
string method = GetArgumentValue(args, "-method") ?? "active";
string by = GetArgumentValue(args, "-by") ?? "name";
string scope = GetArgumentValue(args, "-scope") ?? "children";
// 获取所有元素
var elements = root.FindAll(
TreeScope.Subtree,
Condition.TrueCondition
);
foreach (AutomationElement element in elements)
// 确定搜索范围
TreeScope searchScope = TreeScope.Children;
switch (scope.ToLower())
{
// 如果指定了过滤条件,检查元素名称是否包含过滤文本
if (!string.IsNullOrEmpty(filter) &&
!element.Current.Name.Contains(filter) &&
!element.Current.ClassName.Contains(filter) &&
!element.Current.AutomationId.Contains(filter))
{
continue;
}
case "descendants":
searchScope = TreeScope.Descendants;
break;
case "subtree":
searchScope = TreeScope.Subtree;
break;
}
var elementInfo = new Dictionary<string, string>();
elementInfo.Add("id", CacheElement(element));
elementInfo.Add("name", element.Current.Name);
elementInfo.Add("class", element.Current.ClassName);
elementInfo.Add("type", element.Current.ControlType.ProgrammaticName);
elementInfo.Add("automationId", element.Current.AutomationId);
elementInfo.Add("path", GetElementPath(element));
result.Add(elementInfo);
Condition searchCondition = Condition.TrueCondition;
// 如果指定了过滤条件,创建一个组合条件
if (!string.IsNullOrEmpty(filter))
{
// 尝试直接使用ControlType
if (filter.StartsWith("ControlType."))
{
string controlTypeName = filter.Substring("ControlType.".Length);
ControlType controlType = (ControlType)typeof(ControlType).GetField(controlTypeName).GetValue(null);
searchCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, controlType);
}
else
{
switch (by.ToLower())
{
case "name":
searchCondition = new PropertyCondition(AutomationElement.NameProperty, filter, PropertyConditionFlags.IgnoreCase);
break;
case "class":
searchCondition = new PropertyCondition(AutomationElement.ClassNameProperty, filter, PropertyConditionFlags.IgnoreCase);
break;
case "automation":
searchCondition = new PropertyCondition(AutomationElement.AutomationIdProperty, filter, PropertyConditionFlags.IgnoreCase);
break;
case "xpath":
// XPath暂不支持
throw new Exception("XPath查找暂未实现");
default:
// 如果没有指定查找方式,则使用组合条件
searchCondition = new OrCondition(
new PropertyCondition(AutomationElement.NameProperty, filter, PropertyConditionFlags.IgnoreCase),
new PropertyCondition(AutomationElement.ClassNameProperty, filter, PropertyConditionFlags.IgnoreCase),
new PropertyCondition(AutomationElement.AutomationIdProperty, filter, PropertyConditionFlags.IgnoreCase)
);
break;
}
}
}
// 如果是按标题搜索,先找到所有匹配的窗口
if (method.ToLower() == "title" && !string.IsNullOrEmpty(window))
{
searchScope = TreeScope.Children;
var windows = root.FindAll(searchScope, Condition.TrueCondition);
foreach (AutomationElement win in windows)
{
if (win.Current.Name.Contains(window))
{
// 对每个匹配的窗口进行处理
var windowElements = win.FindAll(TreeScope.Subtree, searchCondition);
foreach (AutomationElement element in windowElements)
{
var elementInfo = new Dictionary<string, string>();
elementInfo.Add("id", CacheElement(element));
elementInfo.Add("name", element.Current.Name);
elementInfo.Add("class", element.Current.ClassName);
elementInfo.Add("controlType", element.Current.ControlType.ProgrammaticName);
elementInfo.Add("type", element.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""));
elementInfo.Add("localizedType", element.Current.LocalizedControlType);
elementInfo.Add("automationId", element.Current.AutomationId);
elementInfo.Add("path", GetElementPath(element));
// 添加控件文本值
try
{
var valuePattern = element.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
if (valuePattern != null)
{
elementInfo.Add("value", valuePattern.Current.Value);
}
}
catch { }
result.Add(elementInfo);
}
}
}
}
else
{
// 其他查找方式保持原有逻辑
var elements = root.FindAll(searchScope, searchCondition);
foreach (AutomationElement element in elements)
{
var elementInfo = new Dictionary<string, string>();
elementInfo.Add("id", CacheElement(element));
elementInfo.Add("name", element.Current.Name);
elementInfo.Add("class", element.Current.ClassName);
elementInfo.Add("controlType", element.Current.ControlType.ProgrammaticName);
elementInfo.Add("type", element.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""));
elementInfo.Add("localizedType", element.Current.LocalizedControlType);
elementInfo.Add("automationId", element.Current.AutomationId);
elementInfo.Add("path", GetElementPath(element));
// 添加控件文本值
try
{
var valuePattern = element.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
if (valuePattern != null)
{
elementInfo.Add("value", valuePattern.Current.Value);
}
}
catch { }
result.Add(elementInfo);
}
}
var serializer = new JavaScriptSerializer();
@ -963,7 +1111,15 @@ public class AutomationManager
string window = GetArgumentValue(args, "-window");
string method = GetArgumentValue(args, "-method") ?? "active";
if (string.IsNullOrEmpty(window) && method == "active")
// 如果是active方法直接返回当前活动窗口
if (method.ToLower() == "active")
{
IntPtr activeHandle = GetForegroundWindow();
return AutomationElement.FromHandle(activeHandle);
}
// 其他方法需要检查window参数
if (string.IsNullOrEmpty(window))
{
return AutomationElement.RootElement;
}
@ -977,10 +1133,15 @@ public class AutomationManager
case "class":
// 通过窗口类名查找
return AutomationElement.RootElement.FindFirst(
var classElements = AutomationElement.RootElement.FindAll(
TreeScope.Children,
new PropertyCondition(AutomationElement.ClassNameProperty, window)
);
if (classElements.Count > 0)
{
return classElements[0];
}
break;
case "process":
// 通过进程名查找
@ -991,151 +1152,199 @@ public class AutomationManager
}
break;
case "active":
// 获取当前活动窗口
IntPtr activeHandle = GetForegroundWindow();
return AutomationElement.FromHandle(activeHandle);
case "title":
default:
// 通过窗口标题查找(支持模糊匹配)
var windows = AutomationElement.RootElement.FindAll(
TreeScope.Children,
Condition.TrueCondition
);
foreach (AutomationElement win in windows)
{
if (win.Current.Name.Contains(window))
{
return win;
}
}
break;
// 返回根元素,让调用方自己处理查找逻辑
return AutomationElement.RootElement;
}
throw new Exception("找不到指定的窗口");
}
private static void FindElement(string[] args, string by, string value, string scope)
private static void InspectElement(string[] args)
{
if (string.IsNullOrEmpty(value))
{
throw new Exception("必须指定查找值 (-value)");
}
// 创建一个半透明的全屏窗口来捕获鼠标事件
Form overlayForm = new Form();
overlayForm.FormBorderStyle = FormBorderStyle.None;
overlayForm.WindowState = FormWindowState.Maximized;
overlayForm.TopMost = true;
overlayForm.BackColor = Color.Black;
overlayForm.Opacity = 0.1; // 设置为淡淡的蒙版效果
overlayForm.Cursor = Cursors.Cross;
overlayForm.ShowInTaskbar = false;
AutomationElement root = GetRootElement(args);
TreeScope treeScope = TreeScope.Children;
switch (scope.ToLower())
{
case "descendants":
treeScope = TreeScope.Descendants;
break;
case "subtree":
treeScope = TreeScope.Subtree;
break;
}
bool completed = false;
Condition condition;
switch (by.ToLower())
{
case "name":
condition = new PropertyCondition(AutomationElement.NameProperty, value);
break;
case "class":
condition = new PropertyCondition(AutomationElement.ClassNameProperty, value);
break;
case "automation":
condition = new PropertyCondition(AutomationElement.AutomationIdProperty, value);
break;
case "xpath":
FindElementByXPath(value);
return;
default:
throw new Exception("不支持的查找方式: " + by);
}
overlayForm.MouseClick += (sender, e) => {
if (e.Button == MouseButtons.Left) {
try {
Point clickPosition;
GetCursorPos(out clickPosition);
var elements = root.FindAll(treeScope, condition);
var result = new List<Dictionary<string, string>>();
// 先关闭覆盖窗口
overlayForm.Hide();
foreach (AutomationElement element in elements)
{
var elementInfo = new Dictionary<string, string>();
elementInfo.Add("id", CacheElement(element));
elementInfo.Add("name", element.Current.Name);
elementInfo.Add("class", element.Current.ClassName);
elementInfo.Add("type", element.Current.ControlType.ProgrammaticName);
elementInfo.Add("automationId", element.Current.AutomationId);
result.Add(elementInfo);
}
// 等待一小段时间确保窗口完全消失
Thread.Sleep(100);
var serializer = new JavaScriptSerializer();
Console.Write(serializer.Serialize(result));
}
// 获取点击位置的元素
var element = AutomationElement.FromPoint(clickPosition.ToWindowsPoint());
private static void InspectElement(string[] args, int timeout)
{
Console.WriteLine("请在" + timeout + "秒内点击要识别的元素...");
// 记录当前鼠标位置
DateTime endTime = DateTime.Now.AddSeconds(timeout);
AutomationElement clickedElement = null;
bool wasPressed = false;
while (DateTime.Now < endTime)
{
// 检测鼠标左键点击
bool isPressed = (GetAsyncKeyState(0x01) & 0x8000) != 0;
if (isPressed && !wasPressed) // 鼠标按下瞬间
{
// 获取当前鼠标位置
Point currentPosition;
GetCursorPos(out currentPosition);
try
{
// 从鼠标位置获取元素
clickedElement = AutomationElement.FromPoint(currentPosition.ToWindowsPoint());
if (clickedElement != null && clickedElement != AutomationElement.RootElement)
{
// 构建元素信息
var elementInfo = new Dictionary<string, object>();
elementInfo.Add("id", CacheElement(clickedElement));
elementInfo.Add("name", clickedElement.Current.Name);
elementInfo.Add("class", clickedElement.Current.ClassName);
elementInfo.Add("type", clickedElement.Current.ControlType.ProgrammaticName);
elementInfo.Add("automationId", clickedElement.Current.AutomationId);
elementInfo.Add("path", GetElementPath(clickedElement));
elementInfo.Add("location", new {
x = clickedElement.Current.BoundingRectangle.X,
y = clickedElement.Current.BoundingRectangle.Y,
width = clickedElement.Current.BoundingRectangle.Width,
height = clickedElement.Current.BoundingRectangle.Height
});
elementInfo.Add("patterns", GetSupportedPatterns(clickedElement));
// 生成示例命令
var commands = new List<string>();
if (!string.IsNullOrEmpty(clickedElement.Current.Name))
commands.Add(string.Format("automation.exe -type find -by name -value \"{0}\"", clickedElement.Current.Name));
if (!string.IsNullOrEmpty(clickedElement.Current.AutomationId))
commands.Add(string.Format("automation.exe -type find -by automationid -value \"{0}\"", clickedElement.Current.AutomationId));
elementInfo.Add("commands", commands);
var serializer = new JavaScriptSerializer();
Console.Write(serializer.Serialize(elementInfo));
return;
if (element != null && element != AutomationElement.RootElement) {
InspectElementInfo(element);
completed = true;
}
}
catch
{
// 忽略获取元素时的错误
catch (Exception ex) {
Console.Error.WriteLine(string.Format("Error: {0}", ex.Message));
}
finally {
overlayForm.Close();
}
}
};
wasPressed = isPressed;
Thread.Sleep(100);
overlayForm.KeyPress += (sender, e) => {
if (e.KeyChar == (char)27) { // ESC键
overlayForm.Close();
Environment.Exit(0); // 直接退出程序
}
};
// 在新线程中显示窗口
Thread thread = new Thread(() => {
overlayForm.Show();
while (!completed) {
Application.DoEvents();
Thread.Sleep(100);
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
}
private static void InspectElementInfo(AutomationElement element)
{
Dictionary<string, object> elementInfo = new Dictionary<string, object>();
// 基本信息
elementInfo.Add("id", element.Current.AutomationId);
elementInfo.Add("name", element.Current.Name);
elementInfo.Add("class", element.Current.ClassName);
elementInfo.Add("controlType", element.Current.ControlType.ProgrammaticName);
elementInfo.Add("type", element.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""));
elementInfo.Add("localizedType", element.Current.LocalizedControlType);
elementInfo.Add("automationId", element.Current.AutomationId);
elementInfo.Add("path", GetElementPath(element));
elementInfo.Add("handle", element.Current.NativeWindowHandle);
// 添加控件文本值
try
{
var valuePattern = element.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
if (valuePattern != null)
{
elementInfo.Add("value", valuePattern.Current.Value);
}
}
catch { }
// 获取窗口信息
var walker = TreeWalker.ControlViewWalker;
var parent = element;
while (parent != null && parent != AutomationElement.RootElement && parent.Current.ControlType.Id != UIA_ControlTypeIds.Window)
{
parent = walker.GetParent(parent);
}
throw new Exception("操作超时");
// 添加窗口详细信息
if (parent != null && parent != AutomationElement.RootElement)
{
var processId = parent.Current.ProcessId;
var process = System.Diagnostics.Process.GetProcessById(processId);
elementInfo.Add("window", new
{
title = parent.Current.Name,
@class = parent.Current.ClassName,
handle = parent.Current.NativeWindowHandle,
processName = process.ProcessName,
processId = processId,
processPath = process.MainModule.FileName
});
}
// 添加父元素信息
var immediateParent = walker.GetParent(element);
if (immediateParent != null && immediateParent != AutomationElement.RootElement)
{
elementInfo.Add("parent", new
{
handle = immediateParent.Current.NativeWindowHandle,
path = GetElementPath(immediateParent),
@class = immediateParent.Current.ClassName,
name = immediateParent.Current.Name,
controlType = immediateParent.Current.ControlType.ProgrammaticName,
type = immediateParent.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""),
localizedType = immediateParent.Current.LocalizedControlType,
automationId = immediateParent.Current.AutomationId
});
}
// 位置信息
elementInfo.Add("location", new {
x = element.Current.BoundingRectangle.X,
y = element.Current.BoundingRectangle.Y,
width = element.Current.BoundingRectangle.Width,
height = element.Current.BoundingRectangle.Height
});
// 支持的模式
List<string> patterns = new List<string>();
foreach (AutomationPattern pattern in element.GetSupportedPatterns())
{
patterns.Add(pattern.ProgrammaticName);
}
elementInfo.Add("patterns", patterns);
// 输出 JSON
var serializer = new JavaScriptSerializer();
Console.Write(serializer.Serialize(elementInfo));
}
// 序列化为 JSON 字符串
private static string JsonSerialize(object obj)
{
if (obj == null) return "null";
if (obj is string)
{
return string.Format("\"{0}\"", ((string)obj).Replace("\"", "\\\""));
}
if (obj is bool)
{
return obj.ToString().ToLower();
}
if (obj is int || obj is long || obj is float || obj is double)
{
return obj.ToString();
}
if (obj is Dictionary<string, object>)
{
var dict = obj as Dictionary<string, object>;
var pairs = dict.Select(kvp => string.Format("\"{0}\": {1}", kvp.Key, JsonSerialize(kvp.Value)));
return string.Format("{{{0}}}", string.Join(", ", pairs));
}
if (obj is System.Collections.IEnumerable)
{
var items = ((System.Collections.IEnumerable)obj).Cast<object>().Select(item => JsonSerialize(item));
return string.Format("[{0}]", string.Join(", ", items));
}
return "null";
}
}

View File

@ -4,6 +4,8 @@ const iconv = require("iconv-lite");
const child_process = require("child_process");
const { getQuickcommandFolderFile } = require("../getQuickcommandFile");
let currentChild = null;
const getAssemblyPath = (assembly) => {
const { version } = getCscPath();
const is64bit = process.arch === "x64";
@ -37,15 +39,9 @@ const getAssemblyPath = (assembly) => {
// v3.0/v3.5 路径
path.join(
process.env["ProgramFiles(x86)"] || process.env.ProgramFiles,
"Reference Assemblies",
"Microsoft",
"Framework",
"v3.0",
assembly + ".dll"
),
path.join(
process.env.ProgramFiles,
process.arch === "x64"
? process.env.ProgramFiles
: process.env["ProgramFiles(x86)"],
"Reference Assemblies",
"Microsoft",
"Framework",
@ -81,13 +77,16 @@ const getFeatureReferences = (feature) => {
const formsDll = getAssemblyPath("System.Windows.Forms");
const typesDll = getAssemblyPath("UIAutomationTypes");
const baseDll = getAssemblyPath("WindowsBase");
const drawingDll = getAssemblyPath("System.Drawing");
if (!automationDll) throw new Error("找不到UIAutomationClient.dll");
if (!formsDll) throw new Error("找不到System.Windows.Forms.dll");
if (!typesDll) throw new Error("找不到UIAutomationTypes.dll");
if (!baseDll) throw new Error("找不到WindowsBase.dll");
if (!drawingDll) throw new Error("找不到System.Drawing.dll");
references =
`/reference:"${automationDll}" /reference:"${formsDll}" ` +
`/reference:"${typesDll}" /reference:"${baseDll}" `;
`/reference:"${typesDll}" /reference:"${baseDll}" ` +
`/reference:"${drawingDll}" `;
}
return references;
};
@ -128,10 +127,11 @@ const buildCsharpFeature = async (feature) => {
};
const getCscPath = () => {
const is64bit = process.arch === "x64";
let cscPath = path.join(
process.env.WINDIR,
"Microsoft.NET",
"Framework",
is64bit ? "Framework64" : "Framework",
"v4.0.30319",
"csc.exe"
);
@ -141,7 +141,7 @@ const getCscPath = () => {
cscPath = path.join(
process.env.WINDIR,
"Microsoft.NET",
"Framework",
is64bit ? "Framework64" : "Framework",
"v3.5",
"csc.exe"
);
@ -163,13 +163,17 @@ const getCscPath = () => {
*/
const runCsharpFeature = async (feature, args = [], options = {}) => {
return new Promise(async (reslove, reject) => {
const { alwaysBuildNewExe = false } = options;
const { alwaysBuildNewExe = window.utools.isDev(), killPrevious = true } =
options;
try {
const featureExePath = await getCsharpFeatureExe(
feature,
alwaysBuildNewExe
);
child_process.execFile(
if (killPrevious && currentChild) {
quickcommand.kill(currentChild.pid, "SIGKILL");
}
currentChild = child_process.execFile(
featureExePath,
args,
{

View File

@ -0,0 +1,165 @@
const { runCsharpFeature } = require("../../csharp");
/**
* 列出所有元素
* @param {string} method 窗口类型title/handle/active
* @param {string} window 窗口标题/句柄
* @param {object} options 选项
* @param {string} options.filter 过滤条件
* @param {boolean} options.background 是否后台操作
* @returns {object[]} 元素列表
*/
const listElements = async function (method, window, options = {}) {
const { filter, scope } = options;
const args = ["-type", "list"];
args.push("-method", method);
if (method !== "active" && window) args.push("-window", window);
if (filter) args.push("-filter", filter);
if (scope) args.push("-scope", scope);
const result = await runCsharpFeature("automation", args);
if (result && result.startsWith("Error:")) {
throw new Error(result.substring(7));
}
try {
return JSON.parse(result);
} catch (error) {
console.error("解析元素列表失败:", error);
return null;
}
};
/**
* 点击元素
* @param {string} method 窗口类型title/handle/active
* @param {string} window 窗口标题/句柄
* @param {string} by 查找方式name/class/type/automationid
* @param {string} value 查找值
* @param {object} options 选项
* @param {string} options.pattern 点击模式invoke/toggle
* @param {boolean} options.background 是否后台操作
* @returns {boolean} 是否成功
*/
const clickElement = async function (method, window, by, value, options = {}) {
const { pattern = "invoke", background = false } = options;
const args = ["-type", "click", "-by", by, "-value", value];
args.push("-method", method);
if (method !== "active" && window) args.push("-window", window);
if (pattern) args.push("-pattern", pattern);
if (background) args.push("-background");
const result = await runCsharpFeature("automation", args);
if (result && result.startsWith("Error:")) {
throw new Error(result.substring(7));
}
return true;
};
/**
* 设置元素值
* @param {string} method 窗口类型title/handle/active
* @param {string} window 窗口标题/句柄
* @param {string} by 查找方式name/class/type/automationid
* @param {string} value 查找值
* @param {string} newValue 要设置的值
* @param {object} options 选项
* @param {boolean} options.background 是否后台操作
* @returns {boolean} 是否成功
*/
const setElementValue = async function (
method,
window,
by,
value,
newValue,
options = {}
) {
const { background = false } = options;
const args = [
"-type",
"setvalue",
"-by",
by,
"-value",
value,
"-newvalue",
newValue,
];
args.push("-method", method);
if (method !== "active" && window) args.push("-window", window);
if (background) args.push("-background");
const result = await runCsharpFeature("automation", args);
if (result && result.startsWith("Error:")) {
throw new Error(result.substring(7));
}
return true;
};
/**
* 获取元素值
* @param {string} method 窗口类型title/handle/active
* @param {string} window 窗口标题/句柄
* @param {string} by 查找方式name/class/type/automationid
* @param {string} value 查找值
* @param {object} options 选项
* @param {boolean} options.background 是否后台操作
* @returns {object} 元素值
*/
const getElementValue = async function (
method,
window,
by,
value,
options = {}
) {
const { background = false } = options;
const args = ["-type", "getvalue", "-by", by, "-value", value];
args.push("-method", method);
if (method !== "active" && window) args.push("-window", window);
if (background) args.push("-background");
const result = await runCsharpFeature("automation", args);
if (result && result.startsWith("Error:")) {
throw new Error(result.substring(7));
}
try {
return JSON.parse(result);
} catch (error) {
console.error("解析元素值失败:", error);
return null;
}
};
/**
* 检查元素
* @param {object} options 选项
* @param {number} options.timeout 超时时间()
* @param {boolean} options.background 是否后台操作
* @returns {object} 元素信息
*/
const inspectElement = async function () {
const args = ["-type", "inspect"];
const result = await runCsharpFeature("automation", args);
if (result && result.startsWith("Error:")) {
throw new Error(result.substring(7));
}
try {
return JSON.parse(result);
} catch (error) {
console.error("解析元素信息失败:", error);
return null;
}
};
module.exports = {
listElements,
clickElement,
setElementValue,
getElementValue,
inspectElement,
};

View File

@ -6,6 +6,7 @@ const registry = require("./registry");
const service = require("./service");
const software = require("./software");
const utils = require("./utils");
const automation = require("./automation");
module.exports = {
window,
@ -16,4 +17,5 @@ module.exports = {
service,
software,
utils,
automation,
};

View File

@ -48,7 +48,7 @@ const getQuickcommandIconPath = () => {
};
// 修改对话框函数,使用新的 dialog.cs
const showSystemMessageBox = async function (content, title = "") {
const showSystemMessageBox = async function (content, title = "提示") {
try {
const iconPath = getQuickcommandIconPath();
if (window.utools.isWindows()) {
@ -97,7 +97,7 @@ const showSystemMessageBox = async function (content, title = "") {
}
};
const showSystemInputBox = async function (placeholders, title = "") {
const showSystemInputBox = async function (placeholders, title = "请输入") {
if (!Array.isArray(placeholders)) {
placeholders = [placeholders];
}
@ -163,7 +163,7 @@ const showSystemInputBox = async function (placeholders, title = "") {
}
};
const showSystemConfirmBox = async function (content, title = "") {
const showSystemConfirmBox = async function (content, title = "请确认") {
const iconPath = getQuickcommandIconPath();
if (window.utools.isMacOs()) {
let iconParam = "note";
@ -207,7 +207,7 @@ const showSystemConfirmBox = async function (content, title = "") {
}
};
const showSystemButtonBox = async function (buttons, title = "") {
const showSystemButtonBox = async function (buttons, title = "请选择") {
const iconPath = getQuickcommandIconPath();
if (window.utools.isMacOs()) {
const itemList = buttons.map((item) => `"${item}"`).join(", ");
@ -250,7 +250,7 @@ const showSystemButtonBox = async function (buttons, title = "") {
const itemsList = buttons
.map((btn, index) => `"${index}" "${btn}"`)
.join(" ");
const script2 = `zenity --list --title="${title}" --text="请选择:" --column="序号" --column="选项" ${itemsList} --width=400 --height=300`;
const script2 = `zenity --list --title="${title}" --column="序号" --column="选项" ${itemsList} --width=400 --height=300`;
const result = await execCommand(script2);
if (!result) return {};
const text = result.trim();
@ -263,7 +263,7 @@ const showSystemButtonBox = async function (buttons, title = "") {
}
};
const showSystemTextArea = async function (defaultText = "", title = "") {
const showSystemTextArea = async function (defaultText = "", title = "请输入") {
const iconPath = getQuickcommandIconPath();
if (window.utools.isWindows()) {
const args = [

View File

@ -85,7 +85,9 @@ export default {
},
// ---
isTable() {
return this.runResult?.[0]?.split("\n")?.[1]?.includes("---");
const result = this.runResult?.[0];
if (typeof result !== "string") return false;
return result.split("\n")?.[1]?.includes("---");
},
},
mounted() {

View File

@ -37,13 +37,7 @@ export default defineComponent({
},
//
commonConfig() {
return (
// , excludeConfig[]
this.modelValue.config?.filter(
(_, index) =>
!this.getSelectSubCommand()?.excludeConfig?.includes(index)
) || []
);
return this.modelValue.config || [];
},
// configconfig
subCommandConfig() {

View File

@ -53,14 +53,7 @@ export const networkCommands = {
label: "URL操作",
desc: "URL解析、格式化和参数处理",
icon: "link",
config: [
{
label: "URL",
component: "VariableInput",
icon: "link",
width: "auto",
},
],
config: [],
subCommands: [
{
value: "quickcomposer.network.url.parse",
@ -71,7 +64,6 @@ export const networkCommands = {
value: "quickcomposer.network.url.format",
label: "格式化URL",
icon: "link",
excludeConfig: [0],
config: [
{
label: "协议",
@ -120,7 +112,6 @@ export const networkCommands = {
value: "quickcomposer.network.url.parseQuery",
label: "解析查询字符串",
icon: "search",
excludeConfig: [0],
config: [
{
label: "查询字符串",
@ -133,7 +124,6 @@ export const networkCommands = {
value: "quickcomposer.network.url.formatQuery",
label: "格式化查询字符串",
icon: "edit",
excludeConfig: [0],
config: [
{
label: "参数",
@ -146,7 +136,6 @@ export const networkCommands = {
value: "quickcomposer.network.url.parsePath",
label: "解析路径",
icon: "folder_open",
excludeConfig: [0],
config: [
{
label: "路径",
@ -159,7 +148,6 @@ export const networkCommands = {
value: "quickcomposer.network.url.parseHost",
label: "解析主机名",
icon: "dns",
excludeConfig: [0],
config: [
{
label: "主机名",
@ -173,6 +161,12 @@ export const networkCommands = {
label: "获取参数",
icon: "find_in_page",
config: [
{
label: "URL",
component: "VariableInput",
icon: "link",
width: "auto",
},
{
label: "参数名",
component: "VariableInput",
@ -186,6 +180,12 @@ export const networkCommands = {
label: "添加参数",
icon: "add_circle",
config: [
{
label: "URL",
component: "VariableInput",
icon: "link",
width: "auto",
},
{
label: "参数名",
component: "VariableInput",
@ -205,6 +205,12 @@ export const networkCommands = {
label: "移除参数",
icon: "remove_circle",
config: [
{
label: "URL",
component: "VariableInput",
icon: "link",
width: "auto",
},
{
label: "参数名",
component: "VariableInput",
@ -217,11 +223,27 @@ export const networkCommands = {
value: "quickcomposer.network.url.isAbsolute",
label: "检查绝对URL",
icon: "check_circle",
config: [
{
label: "URL",
component: "VariableInput",
icon: "link",
width: "auto",
},
],
},
{
value: "quickcomposer.network.url.parseComponents",
label: "解析组成部分",
icon: "category",
config: [
{
label: "URL",
component: "VariableInput",
icon: "link",
width: "auto",
},
],
},
],
},
@ -231,20 +253,19 @@ export const networkCommands = {
desc: "DNS解析和查询",
icon: "dns",
isAsync: true,
config: [
{
label: "要查询的域名",
icon: "dns",
component: "VariableInput",
width: "auto",
},
],
config: [],
subCommands: [
{
label: "DNS查询",
value: "quickcomposer.network.dns.lookupHost",
icon: "search",
config: [
{
label: "要查询的域名",
icon: "dns",
component: "VariableInput",
width: "auto",
},
{
label: "IP版本",
icon: "settings_ethernet",
@ -269,42 +290,97 @@ export const networkCommands = {
value: "quickcomposer.network.dns.resolveAll",
label: "解析所有记录",
icon: "all_inclusive",
config: [
{
label: "要查询的域名",
icon: "dns",
component: "VariableInput",
width: "auto",
},
],
},
{
value: "quickcomposer.network.dns.resolveIpv4",
label: "解析IPv4",
icon: "filter_4",
config: [
{
label: "要查询的域名",
icon: "dns",
component: "VariableInput",
width: "auto",
},
],
},
{
value: "quickcomposer.network.dns.resolveIpv6",
label: "解析IPv6",
icon: "filter_6",
config: [
{
label: "要查询的域名",
icon: "dns",
component: "VariableInput",
width: "auto",
},
],
},
{
value: "quickcomposer.network.dns.resolveMxRecords",
label: "解析MX记录",
icon: "mail",
config: [
{
label: "要查询的域名",
icon: "dns",
component: "VariableInput",
width: "auto",
},
],
},
{
value: "quickcomposer.network.dns.resolveTxtRecords",
label: "解析TXT记录",
icon: "text_fields",
config: [
{
label: "要查询的域名",
icon: "dns",
component: "VariableInput",
width: "auto",
},
],
},
{
value: "quickcomposer.network.dns.resolveNsRecords",
label: "解析NS记录",
icon: "dns",
config: [
{
label: "要查询的域名",
icon: "dns",
component: "VariableInput",
width: "auto",
},
],
},
{
value: "quickcomposer.network.dns.resolveCnameRecords",
label: "解析CNAME记录",
icon: "link",
config: [
{
label: "要查询的域名",
icon: "dns",
component: "VariableInput",
width: "auto",
},
],
},
{
value: "quickcomposer.network.dns.reverseResolve",
label: "反向解析",
icon: "swap_horiz",
excludeConfig: [0],
config: [
{
label: "IP地址",

View File

@ -308,35 +308,58 @@ export const systemCommands = {
label: "路径操作",
desc: "路径解析和处理",
icon: "folder",
config: [
{
label: "路径",
component: "VariableInput",
icon: "folder",
width: "auto",
},
],
config: [],
subCommands: [
{
value: "quickcomposer.system.path.normalize",
label: "规范化路径",
icon: "straighten",
config: [
{
label: "路径",
component: "VariableInput",
icon: "folder",
width: "auto",
},
],
},
{
value: "quickcomposer.system.path.parse",
label: "解析路径",
icon: "account_tree",
config: [
{
label: "路径",
component: "VariableInput",
icon: "folder",
width: "auto",
},
],
},
{
value: "quickcomposer.system.path.dirname",
label: "获取目录名",
icon: "folder",
config: [
{
label: "路径",
component: "VariableInput",
icon: "folder",
width: "auto",
},
],
},
{
value: "quickcomposer.system.path.basename",
label: "获取文件名",
icon: "description",
config: [
{
label: "路径",
component: "VariableInput",
icon: "folder",
width: "auto",
},
{
label: "要移除的扩展名",
component: "VariableInput",
@ -349,17 +372,32 @@ export const systemCommands = {
value: "quickcomposer.system.path.extname",
label: "获取扩展名",
icon: "extension",
config: [
{
label: "路径",
component: "VariableInput",
icon: "folder",
width: "auto",
},
],
},
{
value: "quickcomposer.system.path.isAbsolute",
label: "判断绝对路径",
icon: "check_circle",
config: [
{
label: "路径",
component: "VariableInput",
icon: "folder",
width: "auto",
},
],
},
{
value: "quickcomposer.system.path.join",
label: "连接路径",
icon: "add_link",
excludeConfig: [0],
config: [
{
label: "路径片段",
@ -379,7 +417,6 @@ export const systemCommands = {
value: "quickcomposer.system.path.resolve",
label: "解析绝对路径",
icon: "assistant_direction",
excludeConfig: [0],
config: [
{
label: "路径片段",
@ -399,7 +436,6 @@ export const systemCommands = {
value: "quickcomposer.system.path.relative",
label: "计算相对路径",
icon: "compare_arrows",
excludeConfig: [0],
config: [
{
label: "起始路径",
@ -419,7 +455,6 @@ export const systemCommands = {
value: "quickcomposer.system.path.format",
label: "格式化路径",
icon: "format_shapes",
excludeConfig: [0],
config: [
{
label: "根路径",

View File

@ -104,6 +104,32 @@ const registryPaths = [
},
];
const searchWindowConfig = [
{
key: "method",
label: "查找方式",
component: "q-select",
icon: "search",
width: 3,
options: [
{ label: "标题", value: "title" },
// { label: "类名", value: "class" },
{ label: "句柄", value: "handle" },
// { label: "进程", value: "process" },
{ label: "活动窗口", value: "active" },
],
defaultValue: "title",
},
{
key: "value",
label: "窗口标题/句柄",
component: "VariableInput",
icon: "title",
width: 9,
placeholder: "标题支持模糊匹配,选择活动窗口无需输入",
},
];
export const windowsCommands = {
label: "Win自动化",
icon: "window",
@ -114,29 +140,7 @@ export const windowsCommands = {
label: "窗口控制",
desc: "Windows窗口操作",
icon: "window",
config: [
{
key: "method",
label: "查找方式",
component: "q-select",
icon: "search",
width: 3,
options: [
{ label: "标题", value: "title" },
{ label: "句柄", value: "handle" },
{ label: "活动窗口", value: "active" },
],
defaultValue: "title",
},
{
key: "value",
label: "窗口标题/句柄",
component: "VariableInput",
icon: "title",
width: 9,
placeholder: "标题支持模糊匹配,选择活动窗口无需输入",
},
],
config: searchWindowConfig,
subCommands: [
{
value: "quickcomposer.windows.window.getWindowInfo",
@ -357,7 +361,7 @@ export const windowsCommands = {
items: controlClass,
},
width: 8,
placeholder: "可选,输入要过滤的控件类或文本",
placeholder: "可选,输入要过滤的控件类或文本",
},
background: {
label: "后台操作",
@ -394,7 +398,7 @@ export const windowsCommands = {
width: 12,
options: {
control: {
label: "控件类",
label: "控件类",
component: "VariableInput",
icon: "class",
options: {
@ -408,7 +412,7 @@ export const windowsCommands = {
component: "VariableInput",
icon: "text_fields",
width: 6,
placeholder: "可选,和控件类至少输入一个",
placeholder: "可选,和控件类至少输入一个",
},
pos: {
label: "坐标",
@ -1097,5 +1101,175 @@ export const windowsCommands = {
},
],
},
{
value: "quickcomposer.windows.automation.inspectElement",
label: "UI自动化",
desc: "Windows界面自动化操作",
icon: "smart_button",
isAsync: true,
config: [],
subCommands: [
{
value: "quickcomposer.windows.automation.inspectElement",
label: "检查元素",
icon: "search",
},
{
value: "quickcomposer.windows.automation.listElements",
label: "列出元素",
icon: "list",
config: [
...searchWindowConfig,
{
component: "OptionEditor",
icon: "settings",
width: 12,
options: {
scope: {
label: "范围",
component: "q-select",
icon: "account_tree",
width: 3,
options: [
{ label: "子元素", value: "children" },
{ label: "所有后代", value: "descendants" },
{ label: "整个子树", value: "subtree" },
],
},
filter: {
label: "过滤条件",
component: "VariableInput",
icon: "filter_alt",
width: 9,
placeholder: "可选,按名称/类名/ControlType/AutomationId过滤",
},
},
defaultValue: {
scope: "children",
},
},
],
},
{
value: "quickcomposer.windows.automation.clickElement",
label: "点击元素",
icon: "mouse",
config: [
...searchWindowConfig,
{
key: "by",
label: "查找方式",
component: "q-select",
icon: "search",
width: 4,
options: [
{ label: "名称", value: "name" },
{ label: "类名", value: "class" },
{ label: "类型", value: "type" },
{ label: "AutomationId", value: "automationid" },
],
defaultValue: "name",
},
{
key: "value",
label: "查找值",
component: "VariableInput",
icon: "text_fields",
width: 8,
placeholder: "要点击的元素值",
},
{
key: "pattern",
label: "点击模式",
component: "ButtonGroup",
icon: "touch_app",
width: 12,
options: [
{ label: "普通点击", value: "invoke" },
{ label: "切换状态", value: "toggle" },
],
defaultValue: "invoke",
},
{
key: "background",
label: "后台操作",
component: "CheckButton",
icon: "back_hand",
width: 12,
},
],
},
{
value: "quickcomposer.windows.automation.setElementValue",
label: "设置值",
icon: "edit",
config: [
...searchWindowConfig,
{
key: "by",
label: "查找方式",
component: "q-select",
icon: "search",
width: 4,
options: [
{ label: "名称", value: "name" },
{ label: "类名", value: "class" },
{ label: "类型", value: "type" },
{ label: "AutomationId", value: "automationid" },
],
defaultValue: "name",
},
{
key: "value",
label: "查找值",
component: "VariableInput",
icon: "text_fields",
width: 8,
placeholder: "要设置值的元素",
},
{
key: "newValue",
label: "新值",
component: "VariableInput",
icon: "edit",
width: 12,
placeholder: "要设置的新值",
},
],
},
{
value: "quickcomposer.windows.automation.getElementValue",
label: "获取值",
icon: "content_paste",
outputVariable: "elementValue",
saveOutput: true,
config: [
...searchWindowConfig,
{
key: "by",
label: "查找方式",
component: "q-select",
icon: "search",
width: 4,
options: [
{ label: "名称", value: "name" },
{ label: "类名", value: "class" },
{ label: "类型", value: "type" },
{ label: "AutomationId", value: "automationid" },
],
defaultValue: "name",
},
{
key: "value",
label: "查找值",
component: "VariableInput",
icon: "text_fields",
width: 8,
placeholder: "要获取值的元素",
},
],
},
],
},
],
};