异步执行JS获取元素坐标
概述
本文档提供了使用FBro异步执行JavaScript获取页面元素坐标的完整应用示例。通过封装通用的JavaScript执行方法和元素定位功能,实现精确的元素坐标获取和智能交互操作。
核心方法
通用JavaScript执行方法
csharp
/// <summary>
/// 异步执行JavaScript获取返回值的通用方法
/// </summary>
/// <param name="browser">浏览器实例</param>
/// <param name="jsCode">要执行的JavaScript代码</param>
/// <param name="timeoutMs">超时时间(毫秒),默认10秒</param>
/// <returns>执行结果,失败返回null</returns>
public static string ExecuteJavaScriptWithReturn(IFBroSharpBrowser browser, string jsCode, int timeoutMs = 10000)
{
try
{
FBroSharpJsCallbackSyn callback = new FBroSharpJsCallbackSyn();
browser.GetMainFrame().ExecuteJavaScriptToHasReturn(jsCode, default, default, callback);
if (callback.help != null && callback.help.IsValid)
{
callback.help.WaitEvent(timeoutMs);
if (callback.help.HavStringData())
{
return callback.help.GetStringData();
}
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"执行JavaScript失败: {ex.Message}");
return null;
}
}元素坐标获取方法
csharp
/// <summary>
/// 获取页面元素的坐标信息
/// </summary>
/// <param name="browser">浏览器实例</param>
/// <param name="selector">CSS选择器或XPath</param>
/// <param name="useXPath">是否使用XPath,默认false(使用CSS选择器)</param>
/// <returns>元素坐标信息,包含x,y,width,height等</returns>
public static ElementPosition GetElementPosition(IFBroSharpBrowser browser, string selector, bool useXPath = false)
{
string jsCode;
if (useXPath)
{
// 使用XPath查找元素
jsCode = $@"
(function() {{
try {{
// 使用XPath查找元素
var xpath = ""{selector}"";
var element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (!element) {{
return JSON.stringify({{ error: '元素未找到', selector: xpath }});
}}
// 获取元素的边界矩形
var rect = element.getBoundingClientRect();
var computedStyle = window.getComputedStyle(element);
return JSON.stringify({{
success: true,
selector: xpath,
selectorType: 'xpath',
position: {{
x: Math.round(rect.left + window.pageXOffset),
y: Math.round(rect.top + window.pageYOffset),
width: Math.round(rect.width),
height: Math.round(rect.height),
centerX: Math.round(rect.left + window.pageXOffset + rect.width / 2),
centerY: Math.round(rect.top + window.pageYOffset + rect.height / 2)
}},
viewport: {{
x: Math.round(rect.left),
y: Math.round(rect.top),
right: Math.round(rect.right),
bottom: Math.round(rect.bottom)
}},
element: {{
tagName: element.tagName,
id: element.id || '',
className: element.className || '',
visible: computedStyle.display !== 'none' && computedStyle.visibility !== 'hidden',
offsetParent: element.offsetParent !== null
}},
scroll: {{
pageX: window.pageXOffset,
pageY: window.pageYOffset
}}
}});
}} catch (e) {{
return JSON.stringify({{ error: e.message, selector: ""{selector}"" }});
}}
}})();
";
}
else
{
// 使用CSS选择器查找元素
jsCode = $@"
(function() {{
try {{
// 使用CSS选择器查找元素
var selector = ""{selector}"";
var element = document.querySelector(selector);
if (!element) {{
return JSON.stringify({{ error: '元素未找到', selector: selector }});
}}
// 获取元素的边界矩形
var rect = element.getBoundingClientRect();
var computedStyle = window.getComputedStyle(element);
return JSON.stringify({{
success: true,
selector: selector,
selectorType: 'css',
position: {{
x: Math.round(rect.left + window.pageXOffset),
y: Math.round(rect.top + window.pageYOffset),
width: Math.round(rect.width),
height: Math.round(rect.height),
centerX: Math.round(rect.left + window.pageXOffset + rect.width / 2),
centerY: Math.round(rect.top + window.pageYOffset + rect.height / 2)
}},
viewport: {{
x: Math.round(rect.left),
y: Math.round(rect.top),
right: Math.round(rect.right),
bottom: Math.round(rect.bottom)
}},
element: {{
tagName: element.tagName,
id: element.id || '',
className: element.className || '',
visible: computedStyle.display !== 'none' && computedStyle.visibility !== 'hidden',
offsetParent: element.offsetParent !== null
}},
scroll: {{
pageX: window.pageXOffset,
pageY: window.pageYOffset
}}
}});
}} catch (e) {{
return JSON.stringify({{ error: e.message, selector: ""{selector}"" }});
}}
}})();
";
}
// 使用封装的方法执行JavaScript
string result = ExecuteJavaScriptWithReturn(browser, jsCode, 5000);
if (string.IsNullOrEmpty(result))
{
return new ElementPosition
{
Success = false,
Error = "JavaScript执行失败或超时"
};
}
try
{
return Newtonsoft.Json.JsonConvert.DeserializeObject<ElementPosition>(result);
}
catch (Exception ex)
{
return new ElementPosition
{
Success = false,
Error = $"解析返回结果失败: {ex.Message}"
};
}
}数据结构
元素位置信息
csharp
/// <summary>
/// 元素位置信息数据结构
/// </summary>
public class ElementPosition
{
/// <summary>是否成功获取</summary>
public bool Success { get; set; }
/// <summary>错误信息</summary>
public string Error { get; set; }
/// <summary>选择器</summary>
public string Selector { get; set; }
/// <summary>选择器类型:css 或 xpath</summary>
public string SelectorType { get; set; }
/// <summary>位置信息</summary>
public PositionInfo Position { get; set; }
/// <summary>视口相对位置</summary>
public ViewportInfo Viewport { get; set; }
/// <summary>元素信息</summary>
public ElementInfo Element { get; set; }
/// <summary>页面滚动信息</summary>
public ScrollInfo Scroll { get; set; }
}
/// <summary>
/// 位置信息
/// </summary>
public class PositionInfo
{
/// <summary>相对于页面的X坐标</summary>
public int X { get; set; }
/// <summary>相对于页面的Y坐标</summary>
public int Y { get; set; }
/// <summary>元素宽度</summary>
public int Width { get; set; }
/// <summary>元素高度</summary>
public int Height { get; set; }
/// <summary>元素中心点X坐标</summary>
public int CenterX { get; set; }
/// <summary>元素中心点Y坐标</summary>
public int CenterY { get; set; }
}
/// <summary>
/// 视口信息
/// </summary>
public class ViewportInfo
{
/// <summary>相对于视口的X坐标</summary>
public int X { get; set; }
/// <summary>相对于视口的Y坐标</summary>
public int Y { get; set; }
/// <summary>右边界</summary>
public int Right { get; set; }
/// <summary>下边界</summary>
public int Bottom { get; set; }
}
/// <summary>
/// 元素信息
/// </summary>
public class ElementInfo
{
/// <summary>标签名</summary>
public string TagName { get; set; }
/// <summary>ID属性</summary>
public string Id { get; set; }
/// <summary>Class属性</summary>
public string ClassName { get; set; }
/// <summary>是否可见</summary>
public bool Visible { get; set; }
/// <summary>是否有offset父元素</summary>
public bool OffsetParent { get; set; }
}
/// <summary>
/// 滚动信息
/// </summary>
public class ScrollInfo
{
/// <summary>水平滚动位置</summary>
public int PageX { get; set; }
/// <summary>垂直滚动位置</summary>
public int PageY { get; set; }
}基础应用示例
基本坐标获取
csharp
/// <summary>
/// 使用示例
/// </summary>
public class ElementCoordinateExample
{
public static void DemoElementPosition(IFBroSharpBrowser browser)
{
// 示例1:获取按钮坐标(CSS选择器)
var buttonPos = GetElementPosition(browser, "#submit-button");
if (buttonPos.Success)
{
Console.WriteLine("=== 按钮位置信息 ===");
Console.WriteLine($"选择器: {buttonPos.Selector} ({buttonPos.SelectorType})");
Console.WriteLine($"页面坐标: ({buttonPos.Position.X}, {buttonPos.Position.Y})");
Console.WriteLine($"尺寸: {buttonPos.Position.Width} x {buttonPos.Position.Height}");
Console.WriteLine($"中心点: ({buttonPos.Position.CenterX}, {buttonPos.Position.CenterY})");
Console.WriteLine($"视口坐标: ({buttonPos.Viewport.X}, {buttonPos.Viewport.Y})");
Console.WriteLine($"元素类型: {buttonPos.Element.TagName}");
Console.WriteLine($"是否可见: {buttonPos.Element.Visible}");
Console.WriteLine($"滚动位置: ({buttonPos.Scroll.PageX}, {buttonPos.Scroll.PageY})");
}
else
{
Console.WriteLine($"获取按钮位置失败: {buttonPos.Error}");
}
// 示例2:获取输入框坐标(XPath)
var inputPos = GetElementPosition(browser, "//input[@type='text']", true);
if (inputPos.Success)
{
Console.WriteLine("\n=== 输入框位置信息 ===");
Console.WriteLine($"坐标: ({inputPos.Position.X}, {inputPos.Position.Y})");
Console.WriteLine($"中心点坐标: ({inputPos.Position.CenterX}, {inputPos.Position.CenterY})");
// 可以结合鼠标事件使用
// 点击元素中心
// browser.GetHost().SendMouseClickEvent(inputPos.Position.CenterX, inputPos.Position.CenterY, ...);
}
// 示例3:批量获取多个元素坐标
string[] selectors = { "#header", ".navigation", "footer", "#main-content" };
Console.WriteLine("\n=== 批量获取元素坐标 ===");
foreach (string selector in selectors)
{
var pos = GetElementPosition(browser, selector);
if (pos.Success)
{
Console.WriteLine($"{selector}: ({pos.Position.X}, {pos.Position.Y}) - {pos.Position.Width}x{pos.Position.Height}");
}
else
{
Console.WriteLine($"{selector}: 获取失败 - {pos.Error}");
}
}
// 示例4:检查元素是否在视口内
var elementPos = GetElementPosition(browser, ".article");
if (elementPos.Success)
{
bool isInViewport = elementPos.Viewport.X >= 0 &&
elementPos.Viewport.Y >= 0 &&
elementPos.Viewport.Right <= browser.GetSize().width &&
elementPos.Viewport.Bottom <= browser.GetSize().height;
Console.WriteLine($"\n元素是否在视口内: {isInViewport}");
if (!isInViewport)
{
Console.WriteLine("需要滚动页面才能看到此元素");
// 可以使用JavaScript滚动到元素位置
string scrollJs = $"document.querySelector('{elementPos.Selector}').scrollIntoView({{behavior: 'smooth', block: 'center'}});";
ExecuteJavaScriptWithReturn(browser, scrollJs);
}
}
}
}高级应用
智能点击元素
csharp
/// <summary>
/// 高级应用:智能点击元素(自动滚动到可见区域)
/// </summary>
public static bool SmartClickElement(IFBroSharpBrowser browser, string selector, bool useXPath = false)
{
var elementPos = GetElementPosition(browser, selector, useXPath);
if (!elementPos.Success)
{
Console.WriteLine($"无法获取元素位置: {elementPos.Error}");
return false;
}
// 检查元素是否可见
if (!elementPos.Element.Visible)
{
Console.WriteLine("元素不可见,无法点击");
return false;
}
// 如果元素不在视口内,先滚动到该元素
var browserSize = browser.GetSize();
bool needScroll = elementPos.Viewport.X < 0 ||
elementPos.Viewport.Y < 0 ||
elementPos.Viewport.Right > browserSize.width ||
elementPos.Viewport.Bottom > browserSize.height;
if (needScroll)
{
Console.WriteLine("元素不在视口内,正在滚动到元素位置...");
string scrollJs = useXPath ?
$"document.evaluate('{selector}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.scrollIntoView({{behavior: 'smooth', block: 'center'}});" :
$"document.querySelector('{selector}').scrollIntoView({{behavior: 'smooth', block: 'center'}});";
ExecuteJavaScriptWithReturn(browser, scrollJs);
// 等待滚动完成,重新获取位置
System.Threading.Thread.Sleep(1000);
elementPos = GetElementPosition(browser, selector, useXPath);
if (!elementPos.Success)
{
Console.WriteLine("滚动后重新获取位置失败");
return false;
}
}
// 点击元素中心点
Console.WriteLine($"点击元素中心点: ({elementPos.Position.CenterX}, {elementPos.Position.CenterY})");
// 这里需要根据实际的鼠标事件API调用
// browser.GetHost().SendMouseClickEvent(elementPos.Position.CenterX, elementPos.Position.CenterY, ...);
return true;
}特殊场景处理
动态元素等待
csharp
/// <summary>
/// 处理动态加载元素的坐标获取
/// </summary>
public static ElementPosition WaitAndGetElementPosition(IFBroSharpBrowser browser, string selector,
bool useXPath = false, int maxWaitMs = 10000, int intervalMs = 500)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
while (stopwatch.ElapsedMilliseconds < maxWaitMs)
{
var position = GetElementPosition(browser, selector, useXPath);
if (position.Success && position.Element.Visible)
{
Console.WriteLine($"元素已找到,等待时间: {stopwatch.ElapsedMilliseconds}ms");
return position;
}
System.Threading.Thread.Sleep(intervalMs);
}
return new ElementPosition
{
Success = false,
Error = $"等待元素出现超时({maxWaitMs}ms)"
};
}元素关系分析
csharp
/// <summary>
/// 获取多个元素的相对位置关系
/// </summary>
public static void AnalyzeElementsRelativePosition(IFBroSharpBrowser browser,
string selector1, string selector2)
{
var pos1 = GetElementPosition(browser, selector1);
var pos2 = GetElementPosition(browser, selector2);
if (pos1.Success && pos2.Success)
{
int deltaX = pos2.Position.X - pos1.Position.X;
int deltaY = pos2.Position.Y - pos1.Position.Y;
double distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
Console.WriteLine("=== 元素相对位置分析 ===");
Console.WriteLine($"元素1 ({selector1}): ({pos1.Position.X}, {pos1.Position.Y})");
Console.WriteLine($"元素2 ({selector2}): ({pos2.Position.X}, {pos2.Position.Y})");
Console.WriteLine($"水平距离: {deltaX}px");
Console.WriteLine($"垂直距离: {deltaY}px");
Console.WriteLine($"直线距离: {distance:F2}px");
string relation = "";
if (deltaY < -10) relation += "元素2在元素1上方 ";
else if (deltaY > 10) relation += "元素2在元素1下方 ";
if (deltaX < -10) relation += "元素2在元素1左侧";
else if (deltaX > 10) relation += "元素2在元素1右侧";
if (string.IsNullOrEmpty(relation)) relation = "元素基本重叠";
Console.WriteLine($"相对关系: {relation}");
}
}实际应用场景
1. 表单自动化填写
csharp
/// <summary>
/// 自动化表单填写示例
/// </summary>
public static async Task AutoFillForm(IFBroSharpBrowser browser)
{
// 等待表单加载
var formPos = WaitAndGetElementPosition(browser, "#user-form", maxWaitMs: 5000);
if (!formPos.Success)
{
Console.WriteLine("表单未找到");
return;
}
// 填写用户名
var usernamePos = GetElementPosition(browser, "#username");
if (usernamePos.Success && usernamePos.Element.Visible)
{
// 点击输入框
// browser.GetHost().SendMouseClickEvent(usernamePos.Position.CenterX, usernamePos.Position.CenterY, ...);
// 输入内容
string inputJs = "document.querySelector('#username').value = 'testuser';";
ExecuteJavaScriptWithReturn(browser, inputJs);
}
// 填写密码
var passwordPos = GetElementPosition(browser, "#password");
if (passwordPos.Success && passwordPos.Element.Visible)
{
string inputJs = "document.querySelector('#password').value = 'password123';";
ExecuteJavaScriptWithReturn(browser, inputJs);
}
// 点击提交按钮
SmartClickElement(browser, "#submit-btn");
}2. 页面截图定位
csharp
/// <summary>
/// 基于元素位置的页面截图
/// </summary>
public static void CaptureElementScreenshot(IFBroSharpBrowser browser, string selector)
{
var elementPos = GetElementPosition(browser, selector);
if (elementPos.Success)
{
// 获取元素区域
int x = elementPos.Position.X;
int y = elementPos.Position.Y;
int width = elementPos.Position.Width;
int height = elementPos.Position.Height;
Console.WriteLine($"元素截图区域: ({x}, {y}) - {width}x{height}");
// 这里可以调用FBro的截图功能,截取指定区域
// browser.CaptureScreenshot(x, y, width, height, "element.png");
}
}3. 响应式布局测试
csharp
/// <summary>
/// 响应式布局测试
/// </summary>
public static void TestResponsiveLayout(IFBroSharpBrowser browser)
{
// 测试不同屏幕尺寸下的元素位置
var sizes = new[]
{
new { Width = 1920, Height = 1080, Name = "桌面" },
new { Width = 1024, Height = 768, Name = "平板" },
new { Width = 375, Height = 667, Name = "手机" }
};
foreach (var size in sizes)
{
Console.WriteLine($"\n=== {size.Name}尺寸测试 ({size.Width}x{size.Height}) ===");
// 调整浏览器尺寸
// browser.SetSize(size.Width, size.Height);
// 等待布局调整完成
System.Threading.Thread.Sleep(1000);
// 测试关键元素位置
var headerPos = GetElementPosition(browser, "#header");
var navPos = GetElementPosition(browser, ".navigation");
var contentPos = GetElementPosition(browser, "#main-content");
if (headerPos.Success)
Console.WriteLine($"头部: ({headerPos.Position.X}, {headerPos.Position.Y}) - {headerPos.Position.Width}x{headerPos.Position.Height}");
if (navPos.Success)
Console.WriteLine($"导航: ({navPos.Position.X}, {navPos.Position.Y}) - {navPos.Position.Width}x{navPos.Position.Height}");
if (contentPos.Success)
Console.WriteLine($"内容: ({contentPos.Position.X}, {contentPos.Position.Y}) - {contentPos.Position.Width}x{contentPos.Position.Height}");
}
}性能优化技巧
1. 批量元素查询
csharp
/// <summary>
/// 批量获取多个元素坐标(优化版)
/// </summary>
public static Dictionary<string, ElementPosition> GetMultipleElementPositions(
IFBroSharpBrowser browser, string[] selectors)
{
var result = new Dictionary<string, ElementPosition>();
// 构建批量查询的JavaScript代码
var jsCode = @"
(function() {
var selectors = " + Newtonsoft.Json.JsonConvert.SerializeObject(selectors) + @";
var results = {};
selectors.forEach(function(selector) {
try {
var element = document.querySelector(selector);
if (element) {
var rect = element.getBoundingClientRect();
var computedStyle = window.getComputedStyle(element);
results[selector] = {
success: true,
selector: selector,
selectorType: 'css',
position: {
x: Math.round(rect.left + window.pageXOffset),
y: Math.round(rect.top + window.pageYOffset),
width: Math.round(rect.width),
height: Math.round(rect.height),
centerX: Math.round(rect.left + window.pageXOffset + rect.width / 2),
centerY: Math.round(rect.top + window.pageYOffset + rect.height / 2)
},
element: {
tagName: element.tagName,
id: element.id || '',
className: element.className || '',
visible: computedStyle.display !== 'none' && computedStyle.visibility !== 'hidden'
}
};
} else {
results[selector] = {
success: false,
error: '元素未找到',
selector: selector
};
}
} catch (e) {
results[selector] = {
success: false,
error: e.message,
selector: selector
};
}
});
return JSON.stringify(results);
})();
";
string jsonResult = ExecuteJavaScriptWithReturn(browser, jsCode);
if (!string.IsNullOrEmpty(jsonResult))
{
try
{
var batchResult = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, ElementPosition>>(jsonResult);
return batchResult ?? result;
}
catch (Exception ex)
{
Console.WriteLine($"批量解析失败: {ex.Message}");
}
}
return result;
}使用注意事项
- 坐标准确性:返回的坐标是相对于页面的绝对位置,包含滚动偏移
- 异步执行:建议在非UI线程中调用,避免阻塞界面
- 元素可见性:通过
Element.Visible和Element.OffsetParent判断元素是否真正可见 - 动态内容:对于动态加载的元素,使用
WaitAndGetElementPosition方法 - 错误处理:始终检查
Success属性,处理可能的异常情况 - 性能优化:批量获取时可以合并JavaScript代码减少调用次数
- 坐标系统:区分页面坐标和视口坐标的差异
- 选择器优化:使用高效的CSS选择器或XPath表达式
- 超时设置:根据页面复杂度合理设置超时时间
- 资源管理:注意回调对象的生命周期管理
总结
本文档提供了完整的FBro异步执行JavaScript获取元素坐标的解决方案,包括:
- 🔧 通用方法封装:可复用的JavaScript执行和坐标获取方法
- 📊 丰富数据结构:完整的位置、视口、元素信息
- 🚀 智能交互功能:自动滚动、等待、批量处理
- 🎯 实际应用场景:表单自动化、截图定位、布局测试
- ⚡ 性能优化技巧:批量查询、资源管理
通过这些方法和示例,您可以在FBro项目中实现精确的元素定位和智能的页面交互操作。