Skip to content

异步执行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;
}

使用注意事项

  1. 坐标准确性:返回的坐标是相对于页面的绝对位置,包含滚动偏移
  2. 异步执行:建议在非UI线程中调用,避免阻塞界面
  3. 元素可见性:通过Element.VisibleElement.OffsetParent判断元素是否真正可见
  4. 动态内容:对于动态加载的元素,使用WaitAndGetElementPosition方法
  5. 错误处理:始终检查Success属性,处理可能的异常情况
  6. 性能优化:批量获取时可以合并JavaScript代码减少调用次数
  7. 坐标系统:区分页面坐标和视口坐标的差异
  8. 选择器优化:使用高效的CSS选择器或XPath表达式
  9. 超时设置:根据页面复杂度合理设置超时时间
  10. 资源管理:注意回调对象的生命周期管理

总结

本文档提供了完整的FBro异步执行JavaScript获取元素坐标的解决方案,包括:

  • 🔧 通用方法封装:可复用的JavaScript执行和坐标获取方法
  • 📊 丰富数据结构:完整的位置、视口、元素信息
  • 🚀 智能交互功能:自动滚动、等待、批量处理
  • 🎯 实际应用场景:表单自动化、截图定位、布局测试
  • 性能优化技巧:批量查询、资源管理

通过这些方法和示例,您可以在FBro项目中实现精确的元素定位和智能的页面交互操作。

如果文档对您有帮助,欢迎 请喝咖啡 ☕ | 软件发布 | 源码购买