Skip to content

FBro创建浏览器

概述

FBro创建浏览器是FBro框架的核心功能,允许开发者创建嵌入式浏览器实例。创建浏览器时需要配置窗口信息、浏览器设置以及事件回调处理器。

核心方法

FBroSharpControl.CreatBrowser

语法:

csharp
void CreatBrowser(
    string url,
    FBroSharpWindowsInfo windowsInfo,
    FBroSharpBrowserSetting browserSetting,
    FBroSharpRequestContext requestContext,
    FBroSharpDictionaryValue extraInfo,
    FBroSharpBrowserEvent browserEvent,
    FBroSharpEventDisableControl eventDisableControl,
    string user_flag
)

参数说明:

参数类型必填说明
urlstring初始加载的URL地址
windowsInfoFBroSharpWindowsInfo窗口配置信息
browserSettingFBroSharpBrowserSetting浏览器高级设置
requestContextFBroSharpRequestContext请求上下文(用于独立缓存)
extraInfoFBroSharpDictionaryValue额外参数信息
browserEventFBroSharpBrowserEvent浏览器事件回调处理器
eventDisableControlFBroSharpEventDisableControl事件禁用控制
user_flagstring用户自定义浏览器标识符

user_flag参数详解

作用说明

user_flag参数为每个浏览器实例提供唯一的用户自定义标识符,主要用于:

  1. 浏览器标识:为每个浏览器分配唯一的业务标识
  2. 快速检索:通过FBroSharpBrowserListControl.GetBrowserFromFlag(user_flag)快速获取指定浏览器
  3. 业务关联:将浏览器与特定业务场景、用户会话或任务关联
  4. 批量管理:在多浏览器环境中进行分类和批量操作

使用规范

属性建议
唯一性确保在应用程序范围内唯一
可读性使用有意义的命名,便于调试和维护
长度建议控制在50字符以内
字符规范避免特殊字符,推荐使用字母、数字、下划线、连字符
命名规则采用一致的命名规则,如:模块_功能_序号

user_flag应用示例

基础使用:

csharp
private void CreateBrowserWithFlag_Click(object sender, EventArgs e)
{
    try
    {
        // 生成唯一标识
        string userFlag = $"main_browser_{DateTime.Now:yyyyMMddHHmmss}";
        
        FBroSharpWindowsInfo windows_info = new FBroSharpWindowsInfo
        {
            parent_window = this.Handle,
            x = 0,
            y = 0,
            width = this.ClientSize.Width,
            height = this.ClientSize.Height
        };

        BrowserEvent browser_event = new BrowserEvent();

        // 创建浏览器并指定user_flag
        FBroSharpControl.CreatBrowser(
            "https://www.baidu.com",
            windows_info,
            default,
            default,
            default,
            browser_event,
            default,
            userFlag  // 用户标识
        );

        Console.WriteLine($"浏览器创建完成,标识: {userFlag}");
        
        // 记录浏览器标识,便于后续操作
        SaveBrowserFlag(userFlag);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"创建带标识的浏览器失败: {ex.Message}");
    }
}

/// <summary>
/// 通过user_flag获取浏览器实例
/// </summary>
/// <param name="userFlag">用户标识</param>
/// <returns>浏览器实例</returns>
private FBroSharpBrowser GetBrowserByFlag(string userFlag)
{
    try
    {
        var browser = FBroSharpBrowserListControl.GetBrowserFromFlag(userFlag);
        if (browser != null && browser.IsValid())
        {
            Console.WriteLine($"成功获取浏览器: {userFlag}");
            return browser;
        }
        else
        {
            Console.WriteLine($"未找到标识为 {userFlag} 的浏览器");
            return null;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"获取浏览器失败: {ex.Message}");
        return null;
    }
}

多浏览器管理示例:

csharp
/// <summary>
/// 多浏览器管理器
/// 使用user_flag进行浏览器分类和管理
/// </summary>
public class MultiBrowserManager
{
    private readonly Dictionary<string, BrowserInfo> browserRegistry = new Dictionary<string, BrowserInfo>();
    private readonly object lockObject = new object();

    public class BrowserInfo
    {
        public string Flag { get; set; }
        public string Purpose { get; set; }
        public DateTime CreatedTime { get; set; }
        public string InitialUrl { get; set; }
        public Dictionary<string, object> Properties { get; set; } = new Dictionary<string, object>();
    }

    /// <summary>
    /// 创建分类浏览器
    /// </summary>
    /// <param name="category">浏览器分类</param>
    /// <param name="purpose">用途描述</param>
    /// <param name="url">初始URL</param>
    /// <param name="properties">自定义属性</param>
    /// <returns>浏览器标识</returns>
    public string CreateCategorizedBrowser(string category, string purpose, string url, Dictionary<string, object> properties = null)
    {
        try
        {
            // 生成分类标识
            string timestamp = DateTime.Now.ToString("yyyyMMddHHmmssffff");
            string userFlag = $"{category}_{purpose}_{timestamp}";

            // 记录浏览器信息
            var browserInfo = new BrowserInfo
            {
                Flag = userFlag,
                Purpose = purpose,
                CreatedTime = DateTime.Now,
                InitialUrl = url,
                Properties = properties ?? new Dictionary<string, object>()
            };

            lock (lockObject)
            {
                browserRegistry[userFlag] = browserInfo;
            }

            // 创建窗口配置
            var windowsInfo = CreateCategorizedWindowInfo(category, browserRegistry.Count);

            // 创建额外信息
            var extraInfo = FBroSharpDictionaryValue.Create();
            extraInfo.SetString("category", category);
            extraInfo.SetString("purpose", purpose);
            extraInfo.SetString("user_flag", userFlag);
            extraInfo.SetString("created_time", browserInfo.CreatedTime.ToString());

            // 添加自定义属性
            if (properties != null)
            {
                foreach (var prop in properties)
                {
                    extraInfo.SetString($"prop_{prop.Key}", prop.Value?.ToString() ?? "");
                }
            }

            // 创建事件处理器
            var browserEvent = new CategoryBrowserEvent(userFlag, category, purpose);

            // 创建浏览器
            FBroSharpControl.CreatBrowser(
                url,
                windowsInfo,
                default,
                default,
                extraInfo,
                browserEvent,
                default,
                userFlag
            );

            Console.WriteLine($"分类浏览器创建完成: {category}/{purpose} -> {userFlag}");
            return userFlag;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"创建分类浏览器失败: {ex.Message}");
            return null;
        }
    }

    /// <summary>
    /// 获取指定分类的所有浏览器
    /// </summary>
    /// <param name="category">分类名称</param>
    /// <returns>浏览器列表</returns>
    public List<FBroSharpBrowser> GetBrowsersByCategory(string category)
    {
        var browsers = new List<FBroSharpBrowser>();
        
        lock (lockObject)
        {
            var categoryFlags = browserRegistry.Values
                .Where(info => info.Flag.StartsWith($"{category}_"))
                .Select(info => info.Flag)
                .ToList();

            foreach (var flag in categoryFlags)
            {
                var browser = FBroSharpBrowserListControl.GetBrowserFromFlag(flag);
                if (browser != null && browser.IsValid())
                {
                    browsers.Add(browser);
                }
            }
        }

        return browsers;
    }

    /// <summary>
    /// 批量操作指定分类的浏览器
    /// </summary>
    /// <param name="category">分类名称</param>
    /// <param name="action">操作函数</param>
    public void BatchOperateCategory(string category, Action<FBroSharpBrowser, BrowserInfo> action)
    {
        var browsers = GetBrowsersByCategory(category);
        
        foreach (var browser in browsers)
        {
            try
            {
                var flag = GetBrowserFlag(browser);
                if (!string.IsNullOrEmpty(flag) && browserRegistry.ContainsKey(flag))
                {
                    action(browser, browserRegistry[flag]);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"批量操作浏览器失败: {ex.Message}");
            }
        }
    }

    /// <summary>
    /// 关闭指定分类的所有浏览器
    /// </summary>
    /// <param name="category">分类名称</param>
    public void CloseCategoryBrowsers(string category)
    {
        BatchOperateCategory(category, (browser, info) =>
        {
            Console.WriteLine($"关闭浏览器: {info.Purpose} ({info.Flag})");
            browser.CloseDevTools();
            browser.CloseBrowser(true);
        });
    }

    /// <summary>
    /// 创建分类窗口信息
    /// </summary>
    private FBroSharpWindowsInfo CreateCategorizedWindowInfo(string category, int index)
    {
        // 根据分类设置不同的窗口配置
        switch (category.ToLower())
        {
            case "popup":
                return new FBroSharpWindowsInfo
                {
                    parent_window = IntPtr.Zero,
                    x = 100 + (index * 50),
                    y = 100 + (index * 50),
                    width = 1000,
                    height = 700,
                    window_name = $"弹窗浏览器 {index}"
                };
                
            case "embedded":
                return new FBroSharpWindowsInfo
                {
                    parent_window = IntPtr.Zero,
                    x = 0,
                    y = 0,
                    width = 800,
                    height = 600
                };
                
            case "monitor":
                return new FBroSharpWindowsInfo
                {
                    parent_window = IntPtr.Zero,
                    x = 1920 + (index * 100),  // 第二屏幕
                    y = 100 + (index * 100),
                    width = 1200,
                    height = 800,
                    window_name = $"监控浏览器 {index}"
                };
                
            default:
                return new FBroSharpWindowsInfo
                {
                    parent_window = IntPtr.Zero,
                    x = 200 + (index * 30),
                    y = 200 + (index * 30),
                    width = 1024,
                    height = 768,
                    window_name = $"{category}浏览器 {index}"
                };
        }
    }

    /// <summary>
    /// 获取浏览器的user_flag
    /// </summary>
    private string GetBrowserFlag(FBroSharpBrowser browser)
    {
        // 这里需要根据实际API获取浏览器的user_flag
        // 示例实现,具体实现取决于FBro API
        lock (lockObject)
        {
            foreach (var kvp in browserRegistry)
            {
                var testBrowser = FBroSharpBrowserListControl.GetBrowserFromFlag(kvp.Key);
                if (testBrowser != null && testBrowser.IsSame(browser))
                {
                    return kvp.Key;
                }
            }
        }
        return null;
    }

    private IntPtr GetMainWindowHandle()
    {
        return Process.GetCurrentProcess().MainWindowHandle;
    }

    /// <summary>
    /// 获取浏览器统计信息
    /// </summary>
    public void PrintBrowserStatistics()
    {
        lock (lockObject)
        {
            var categories = browserRegistry.Values
                .GroupBy(info => info.Flag.Split('_')[0])
                .ToDictionary(g => g.Key, g => g.Count());

            Console.WriteLine("浏览器统计信息:");
            foreach (var category in categories)
            {
                Console.WriteLine($"  {category.Key}: {category.Value} 个浏览器");
            }
            
            Console.WriteLine($"总计: {browserRegistry.Count} 个浏览器");
        }
    }
}

/// <summary>
/// 分类浏览器专用事件处理器
/// </summary>
public class CategoryBrowserEvent : BrowserEvent
{
    private readonly string userFlag;
    private readonly string category;
    private readonly string purpose;

    public CategoryBrowserEvent(string userFlag, string category, string purpose)
    {
        this.userFlag = userFlag;
        this.category = category;
        this.purpose = purpose;
    }

    public override void OnAfterCreated(IFBroSharpBrowser browser, IFBroSharpDictionaryValue extrainfo)
    {
        base.OnAfterCreated(browser, extrainfo);
        
        Console.WriteLine($"[{category}] 浏览器创建完成: {purpose} ({userFlag})");
        
        // 注入分类标识信息
        string categoryJS = $@"
            window.FBroBrowserInfo = {{
                flag: '{userFlag}',
                category: '{category}',
                purpose: '{purpose}',
                createdTime: '{DateTime.Now:yyyy-MM-dd HH:mm:ss}'
            }};
            console.log('FBro分类浏览器信息已注入:', window.FBroBrowserInfo);
        ";
        
        browser.GetMainFrame().ExecuteJavaScript(categoryJS, "", 0);
    }

    public override void OnBeforeClose(IFBroSharpBrowser browser)
    {
        Console.WriteLine($"[{category}] 浏览器即将关闭: {purpose} ({userFlag})");
        base.OnBeforeClose(browser);
    }
}

## 浏览器事件回调类

### BrowserEvent类概述

`BrowserEvent`是继承自`FBroSharpBrowserEvent`的自定义事件处理类,用于处理浏览器的各种生命周期事件、用户交互事件和网络事件。

### 完整的BrowserEvent实现示例

```csharp
using FBroSharp;
using FBroSharp.Const;
using FBroSharp.DataType;
using FBroSharp.Event;
using FBroSharp.Lib;
using FBroSharp.Value;
using System;
using System.Collections.Generic;
using System.Reflection;
using FBroSharp.Callback;
using System.IO;

namespace BaseTest
{
    /// <summary>
    /// 浏览器列表管理类
    /// </summary>
    public class BrowserList
    {
        public static List<FBroSharpBrowser> data = new List<FBroSharpBrowser>();
    }

    /// <summary>
    /// 浏览器事件回调处理类
    /// 继承自FBroSharpBrowserEvent,处理浏览器的各种事件
    /// </summary>
    public class BrowserEvent : FBroSharpBrowserEvent
    {
        #region 生命周期事件

        /// <summary>
        /// 浏览器创建完成事件
        /// 在浏览器创建完成后触发,此时可以安全地执行浏览器操作
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <param name="extrainfo">额外信息</param>
        public override void OnAfterCreated(IFBroSharpBrowser browser, IFBroSharpDictionaryValue extrainfo)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 浏览器创建完成");

            // 判断是否为后台浏览器
            bool isBackground = extrainfo?.GetBool("是否为后台") ?? false;
            
            if (!isBackground)
            {
                BrowserList.data.Add((FBroSharpBrowser)browser);
                Console.WriteLine($"前台浏览器创建完成,ID: {browser.GetIdentifier()}");
            }
            else
            {
                Console.WriteLine($"后台浏览器创建完成,ID: {browser.GetIdentifier()}");
            }

            // 设置初始配置
            ConfigureBrowserAfterCreation(browser);
        }

        /// <summary>
        /// 浏览器即将关闭事件
        /// 在浏览器关闭前触发,用于清理资源
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        public override void OnBeforeClose(IFBroSharpBrowser browser)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 浏览器即将关闭");

            // 从浏览器列表中移除
            for (int i = 0; i < BrowserList.data.Count; i++)
            {
                var temp_browser = BrowserList.data[i];
                if (temp_browser.IsSame(browser))
                {
                    BrowserList.data.RemoveAt(i);
                    Console.WriteLine($"浏览器已从列表中移除,剩余数量: {BrowserList.data.Count}");
                    break;
                }
            }

            // 清理浏览器相关资源
            CleanupBrowserResources(browser);
        }

        /// <summary>
        /// 执行关闭事件
        /// 返回false允许关闭,返回true阻止关闭
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <returns>是否阻止关闭</returns>
        public override bool DoClose(IFBroSharpBrowser browser)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 请求关闭浏览器");
            
            // 这里可以添加关闭前的确认逻辑
            // 返回false允许关闭,返回true阻止关闭
            return false;
        }

        #endregion

        #region 页面加载事件

        /// <summary>
        /// 加载状态改变事件
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <param name="isLoading">是否正在加载</param>
        /// <param name="canGoBack">是否可以后退</param>
        /// <param name="canGoForward">是否可以前进</param>
        public override void OnLoadingStateChange(IFBroSharpBrowser browser, bool isLoading, bool canGoBack, bool canGoForward)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 加载状态: {(isLoading ? "加载中" : "加载完成")}");
            
            if (!isLoading)
            {
                Console.WriteLine("页面加载完成,可以执行后续操作");
                OnPageLoadCompleted(browser);
            }
        }

        /// <summary>
        /// 页面开始加载事件
        /// </summary>
        public override void OnLoadStart(IFBroSharpBrowser browser, IFBroSharpFrame frame, FBroSharpTransitionType transition_type)
        {
            if (frame.IsMain())
            {
                Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 主框架开始加载: {frame.GetURL()}");
            }
        }

        /// <summary>
        /// 页面加载完成事件
        /// </summary>
        public override void OnLoadEnd(IFBroSharpBrowser browser, IFBroSharpFrame frame, int httpStatusCode)
        {
            if (frame.IsMain())
            {
                Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 主框架加载完成,状态码: {httpStatusCode}");
            }
        }

        /// <summary>
        /// 页面加载错误事件
        /// </summary>
        public override void OnLoadError(IFBroSharpBrowser browser, IFBroSharpFrame frame, int errorCode, string errorText, string failedUrl)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 加载错误: {errorText} (代码: {errorCode})");
            Console.WriteLine($"失败URL: {failedUrl}");
        }

        #endregion

        #region 页面信息变更事件

        /// <summary>
        /// 页面标题改变事件
        /// </summary>
        public override void OnTitleChange(IFBroSharpBrowser browser, string title)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 页面标题: {title}");
        }

        /// <summary>
        /// 地址栏改变事件
        /// </summary>
        public override void OnAddressChange(IFBroSharpBrowser browser, IFBroSharpFrame frame, string url)
        {
            if (frame.IsMain())
            {
                Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 地址改变: {url}");
            }
        }

        #endregion

        #region 下载事件

        /// <summary>
        /// 即将下载事件
        /// 处理文件下载前的配置
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <param name="download_item">下载项</param>
        /// <param name="suggested_name">建议文件名</param>
        /// <param name="callback">下载回调</param>
        public override void OnBeforeDownload(IFBroSharpBrowser browser, IFBroSharpDownloadItem download_item, string suggested_name, IFBroSharpBeforeDownloadCallback callback)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 准备下载: {suggested_name}");
            
            // 设置下载路径
            string downloadPath = Path.Combine(Directory.GetCurrentDirectory(), "Downloads");
            if (!Directory.Exists(downloadPath))
            {
                Directory.CreateDirectory(downloadPath);
            }
            
            string fullPath = Path.Combine(downloadPath, suggested_name);
            callback.Continue(fullPath, false);
        }

        /// <summary>
        /// 下载进度更新事件
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <param name="download_item">下载项</param>
        /// <param name="callback">下载回调</param>
        public override void OnDownloadUpdated(IFBroSharpBrowser browser, IFBroSharpDownloadItem download_item, IFBroSharpDownloadItemCallback callback)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 下载进度: {download_item.GetPercentComplete()}%");
            Console.WriteLine($"下载速度: {download_item.GetCurrentSpeed()} bytes/s");
            
            if (download_item.IsComplete())
            {
                Console.WriteLine($"下载完成: {download_item.GetFullPath()}");
            }
        }

        #endregion

        #region 右键菜单事件

        /// <summary>
        /// 即将显示右键菜单事件
        /// 可以自定义右键菜单项
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <param name="frame">框架实例</param>
        /// <param name="menu_params">菜单参数</param>
        /// <param name="model">菜单模型</param>
        public override void OnBeforeContextMenu(IFBroSharpBrowser browser, IFBroSharpFrame frame, IFBroSharpContextMenuParams menu_params, IFBroSharpMenuModel model)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 显示右键菜单");
            
            // 添加分隔线
            model.AddSeparator();
            
            // 添加自定义菜单
            IFBroSharpMenuModel sub_model = model.AddSubMenu(10000, "FBro工具");
            sub_model.AddItem(10001, "元素检查");
            sub_model.AddItem(10002, "开发者工具");
            sub_model.AddSeparator();
            sub_model.AddItem(10003, "页面信息");
        }

        /// <summary>
        /// 右键菜单命令执行事件
        /// 处理自定义菜单项的点击
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <param name="frame">框架实例</param>
        /// <param name="menu_params">菜单参数</param>
        /// <param name="command_id">命令ID</param>
        /// <param name="event_flags">事件标志</param>
        /// <returns>是否处理了命令</returns>
        public override bool OnContextMenuCommand(IFBroSharpBrowser browser, IFBroSharpFrame frame, IFBroSharpContextMenuParams menu_params, int command_id, FBroSharpEventFlags event_flags)
        {
            switch (command_id)
            {
                case 10001: // 元素检查
                    var element_at = new FBroSharpElementAt
                    {
                        x = menu_params.GetXCoord(),
                        y = menu_params.GetYCoord()
                    };
                    browser.ShowDevTools("元素检查", IntPtr.Zero, 0, 0, 1024, 900, default, element_at, default, default);
                    return true;
                    
                case 10002: // 开发者工具
                    browser.ShowDevTools("开发者工具", IntPtr.Zero, 0, 0, 1024, 900, default, default, default, default);
                    return true;
                    
                case 10003: // 页面信息
                    ShowPageInfo(browser, frame);
                    return true;
            }
            
            return false;
        }

        #endregion

        #region 弹窗事件

        /// <summary>
        /// 即将打开新窗口事件
        /// 可以控制弹窗行为
        /// </summary>
        public override bool OnBeforePopup(IFBroSharpBrowser browser, IFBroSharpFrame frame, string target_url, string target_frame_name,
            FBroSharpWindowOpenDisposition target_disposition, bool user_gesture, FBroSharpPopupfeatures popupFeatures, ref FBroSharpWindowsInfo windowInfo,
            ref FBroSharpBrowserSetting settings, ref bool no_javascript_access, IFBroSharpUseExtraData user_settings)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 请求打开新窗口: {target_url}");
            
            // 在当前窗口中打开链接,阻止弹窗
            frame.LoadURL(target_url);
            return true; // 返回true阻止弹窗
        }

        #endregion

        #region 证书和安全事件

        /// <summary>
        /// 证书错误事件
        /// 处理SSL证书错误
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <param name="cert_error">证书错误代码</param>
        /// <param name="request_url">请求URL</param>
        /// <param name="ssl_info">SSL信息</param>
        /// <param name="callback">回调</param>
        /// <returns>是否继续访问</returns>
        public override bool OnCertificateError(IFBroSharpBrowser browser, int cert_error, string request_url, IFBroSharpSSLInfo ssl_info, IFBroSharpCallback callback)
        {
            Console.WriteLine($"[{MethodBase.GetCurrentMethod().Name}] 证书错误: {cert_error}");
            Console.WriteLine($"URL: {request_url}");
            
            // 继续访问(忽略证书错误)
            callback.Continue();
            return true;
        }

        #endregion

        #region 控制台消息事件

        /// <summary>
        /// 控制台消息事件
        /// 捕获页面的console输出
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <param name="level">日志级别</param>
        /// <param name="message">消息内容</param>
        /// <param name="source">消息来源</param>
        /// <param name="line">行号</param>
        /// <returns>是否阻止输出到控制台</returns>
        public override bool OnConsoleMessage(IFBroSharpBrowser browser, FBroLogSeverityType level, string message, string source, int line)
        {
            string levelStr = level switch
            {
                FBroLogSeverityType.LOGSEVERITY_DEBUG => "DEBUG",
                FBroLogSeverityType.LOGSEVERITY_INFO => "INFO",
                FBroLogSeverityType.LOGSEVERITY_WARNING => "WARNING",
                FBroLogSeverityType.LOGSEVERITY_ERROR => "ERROR",
                _ => "UNKNOWN"
            };
            
            Console.WriteLine($"[CONSOLE-{levelStr}] {message} (来源: {source}:{line})");
            
            return false; // 返回false允许继续输出到控制台
        }

        #endregion

        #region 自定义辅助方法

        /// <summary>
        /// 浏览器创建完成后的配置
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        private void ConfigureBrowserAfterCreation(IFBroSharpBrowser browser)
        {
            try
            {
                // 设置缩放级别
                browser.SetZoomLevel(0.0);
                
                // 可以在这里添加其他初始化配置
                Console.WriteLine("浏览器初始配置完成");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"配置浏览器失败: {ex.Message}");
            }
        }

        /// <summary>
        /// 页面加载完成后的处理
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        private void OnPageLoadCompleted(IFBroSharpBrowser browser)
        {
            try
            {
                // 可以在这里执行JavaScript或其他操作
                string jsCode = @"
                    console.log('页面加载完成,FBro事件处理器已就绪');
                    document.title = document.title + ' [FBro]';
                ";
                
                browser.GetMainFrame().ExecuteJavaScript(jsCode, "", 0);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"页面加载完成处理失败: {ex.Message}");
            }
        }

        /// <summary>
        /// 显示页面信息
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        /// <param name="frame">框架实例</param>
        private void ShowPageInfo(IFBroSharpBrowser browser, IFBroSharpFrame frame)
        {
            try
            {
                string info = $@"
页面信息:
- URL: {frame.GetURL()}
- 标题: {frame.GetName()}
- 浏览器ID: {browser.GetIdentifier()}
- 是否主框架: {frame.IsMain()}
- 是否有效: {frame.IsValid()}
                ";
                
                Console.WriteLine(info);
                
                // 也可以通过JavaScript在页面中显示
                string jsCode = $@"
                    alert('页面信息:\nURL: {frame.GetURL()}\n浏览器ID: {browser.GetIdentifier()}');
                ";
                
                frame.ExecuteJavaScript(jsCode, "", 0);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"显示页面信息失败: {ex.Message}");
            }
        }

        /// <summary>
        /// 清理浏览器相关资源
        /// </summary>
        /// <param name="browser">浏览器实例</param>
        private void CleanupBrowserResources(IFBroSharpBrowser browser)
        {
            try
            {
                // 在这里可以清理与该浏览器相关的资源
                Console.WriteLine($"清理浏览器资源: {browser.GetIdentifier()}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"清理浏览器资源失败: {ex.Message}");
            }
        }

        #endregion
    }
}

基础创建示例

简单创建浏览器

csharp
private void CreateBasicBrowser_Click(object sender, EventArgs e)
{
    try
    {
        // 1. 生成唯一标识
        string userFlag = $"basic_browser_{DateTime.Now:yyyyMMddHHmmss}";

        // 2. 配置窗口信息
        FBroSharpWindowsInfo windows_info = new FBroSharpWindowsInfo
        {
            parent_window = this.Handle,  // 父窗口句柄
            x = 0,                        // X坐标
            y = 0,                        // Y坐标
            width = this.ClientSize.Width,   // 宽度
            height = this.ClientSize.Height  // 高度
        };

        // 3. 创建浏览器事件回调类
        BrowserEvent browser_event = new BrowserEvent();

        // 4. 创建浏览器
        FBroSharpControl.CreatBrowser(
            "https://www.baidu.com",  // 初始URL
            windows_info,             // 窗口信息
            default,                  // 浏览器设置(使用默认)
            default,                  // 请求上下文(使用默认)
            default,                  // 额外信息(使用默认)
            browser_event,            // 事件回调
            default,                  // 事件禁用控制(使用默认)
            userFlag                  // 用户标识
        );

        Console.WriteLine($"浏览器创建请求已发送,标识: {userFlag}");
        
        // 5. 记录浏览器标识以便后续使用
        SaveBrowserFlag(userFlag);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"创建浏览器失败: {ex.Message}");
    }
}

/// <summary>
/// 保存浏览器标识的辅助方法
/// </summary>
/// <param name="userFlag">用户标识</param>
private void SaveBrowserFlag(string userFlag)
{
    // 可以保存到配置文件、数据库或内存中
    // 这里简单保存到静态列表中
    BrowserFlagManager.RegisterFlag("最新创建的浏览器", userFlag);
    Console.WriteLine($"浏览器标识已保存: {userFlag}");
}

高级创建示例

带完整配置的浏览器创建

csharp
/// <summary>
/// 创建高级配置的浏览器
/// </summary>
/// <param name="url">初始URL</param>
/// <param name="useCustomCache">是否使用自定义缓存</param>
/// <param name="isPopup">是否为弹窗</param>
/// <param name="customFlag">自定义标识(可选)</param>
public void CreateAdvancedBrowser(string url, bool useCustomCache = false, bool isPopup = false, string customFlag = null)
{
    try
    {
        // 1. 生成或使用自定义标识
        string userFlag = customFlag ?? BrowserFlagNaming.GenerateStandardFlag(
            isPopup ? "popup" : "embedded", 
            "advanced", 
            null
        );

        // 2. 验证标识
        var validation = BrowserFlagValidator.ValidateFlag(userFlag);
        if (!validation.IsValid)
        {
            Console.WriteLine($"标识验证失败: {validation}");
            return;
        }

        // 3. 确保标识唯一
        userFlag = BrowserFlagManager.EnsureUniqueFlag(userFlag);

        // 4. 窗口信息配置
        FBroSharpWindowsInfo windows_info = new FBroSharpWindowsInfo();
        
        if (isPopup)
        {
            // 弹窗配置
            windows_info.parent_window = IntPtr.Zero;
            windows_info.x = 100;
            windows_info.y = 100;
            windows_info.width = 1024;
            windows_info.height = 768;
            windows_info.window_name = $"FBro高级浏览器-{userFlag}";
        }
        else
        {
            // 嵌入式配置
            windows_info.parent_window = this.Handle;
            windows_info.x = 0;
            windows_info.y = 0;
            windows_info.width = this.ClientSize.Width;
            windows_info.height = this.ClientSize.Height;
        }

        // 5. 浏览器设置(可选)
        FBroSharpBrowserSetting browser_setting = new FBroSharpBrowserSetting
        {
            // 在这里可以配置浏览器的高级设置
            // 具体设置项需要根据FBroSharpBrowserSetting的属性来配置
        };

        // 6. 请求上下文(自定义缓存)
        FBroSharpRequestContext request_context = null;
        if (useCustomCache)
        {
            var contextSet = new FBroSharpRequestContextSet
            {
                cache_path = Path.Combine(Application.StartupPath, "CustomCache", userFlag)
            };
            request_context = (FBroSharpRequestContext)FBroSharpRequestContext.CreateContext(contextSet);
        }

        // 7. 额外信息
        FBroSharpDictionaryValue extra_info = FBroSharpDictionaryValue.Create();
        extra_info.SetBool("是否为后台", false);
        extra_info.SetString("创建时间", DateTime.Now.ToString());
        extra_info.SetString("用途", "高级浏览器");
        extra_info.SetString("user_flag", userFlag);
        extra_info.SetBool("is_popup", isPopup);
        extra_info.SetBool("use_custom_cache", useCustomCache);

        // 8. 创建事件处理器
        BrowserEvent browser_event = new BrowserEvent();

        // 9. 事件禁用控制(可选)
        FBroSharpEventDisableControl event_disable = new FBroSharpEventDisableControl();
        // 可以在这里禁用某些不需要的事件

        // 10. 创建浏览器
        FBroSharpControl.CreatBrowser(
            url,
            windows_info,
            browser_setting,
            request_context,
            extra_info,
            browser_event,
            event_disable,
            userFlag  // 用户标识
        );

        // 11. 注册浏览器标识
        BrowserFlagManager.RegisterFlag($"高级浏览器-{DateTime.Now:HHmmss}", userFlag);

        Console.WriteLine($"高级浏览器创建请求已发送: {url}, 标识: {userFlag}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"创建高级浏览器失败: {ex.Message}");
        Console.WriteLine($"堆栈跟踪: {ex.StackTrace}");
    }
}

批量创建浏览器

csharp
/// <summary>
/// 批量创建多个浏览器
/// </summary>
/// <param name="urls">URL列表</param>
/// <param name="startX">起始X坐标</param>
/// <param name="startY">起始Y坐标</param>
/// <param name="flagPrefix">标识前缀</param>
public void CreateMultipleBrowsers(string[] urls, int startX = 100, int startY = 100, string flagPrefix = "batch")
{
    for (int i = 0; i < urls.Length; i++)
    {
        try
        {
            // 1. 生成唯一标识
            string userFlag = BrowserFlagNaming.GenerateStandardFlag(flagPrefix, $"browser_{i}", i + 1);

            // 2. 验证并确保标识唯一
            userFlag = BrowserFlagManager.EnsureUniqueFlag(userFlag);

            // 3. 错开窗口位置
            var windows_info = new FBroSharpWindowsInfo
            {
                parent_window = IntPtr.Zero,  // 创建弹窗
                x = startX + (i * 50),        // 错开X坐标
                y = startY + (i * 50),        // 错开Y坐标
                width = 800,
                height = 600,
                window_name = $"批量浏览器 {i + 1} - {userFlag}"
            };

            // 4. 为每个浏览器创建独立的事件处理器
            var browser_event = new BrowserEvent();
            
            // 5. 添加标识信息
            var extra_info = FBroSharpDictionaryValue.Create();
            extra_info.SetInt("浏览器索引", i);
            extra_info.SetString("创建批次", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
            extra_info.SetString("user_flag", userFlag);
            extra_info.SetString("url", urls[i]);
            extra_info.SetInt("total_count", urls.Length);

            // 6. 创建浏览器
            FBroSharpControl.CreatBrowser(
                urls[i],
                windows_info,
                default,
                default,
                extra_info,
                browser_event,
                default,
                userFlag  // 用户标识
            );

            // 7. 注册标识映射
            BrowserFlagManager.RegisterFlag($"批量浏览器-{i + 1}", userFlag);

            Console.WriteLine($"浏览器 {i + 1} 创建请求已发送: {urls[i]}, 标识: {userFlag}");
            
            // 可选:添加延迟避免同时创建过多浏览器
            System.Threading.Thread.Sleep(100);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"创建浏览器 {i + 1} 失败: {ex.Message}");
        }
    }

    Console.WriteLine($"批量创建完成,共请求创建 {urls.Length} 个浏览器");
}

常用事件处理模式

1. 页面加载完成检测

csharp
public class PageLoadDetector : BrowserEvent
{
    private readonly Action<IFBroSharpBrowser> onPageLoadComplete;

    public PageLoadDetector(Action<IFBroSharpBrowser> onComplete)
    {
        onPageLoadComplete = onComplete;
    }

    public override void OnLoadingStateChange(IFBroSharpBrowser browser, bool isLoading, bool canGoBack, bool canGoForward)
    {
        if (!isLoading)
        {
            onPageLoadComplete?.Invoke(browser);
        }
    }
}

// 使用示例
var detector = new PageLoadDetector(browser => {
    Console.WriteLine("页面加载完成,开始执行业务逻辑");
    // 执行页面加载完成后的操作
});

2. 自动下载处理

csharp
public class AutoDownloadHandler : BrowserEvent
{
    private readonly string downloadDirectory;

    public AutoDownloadHandler(string downloadDir)
    {
        downloadDirectory = downloadDir;
        if (!Directory.Exists(downloadDirectory))
        {
            Directory.CreateDirectory(downloadDirectory);
        }
    }

    public override void OnBeforeDownload(IFBroSharpBrowser browser, IFBroSharpDownloadItem download_item, string suggested_name, IFBroSharpBeforeDownloadCallback callback)
    {
        string filepath = Path.Combine(downloadDirectory, suggested_name);
        callback.Continue(filepath, false);
        Console.WriteLine($"开始下载: {suggested_name} -> {filepath}");
    }

    public override void OnDownloadUpdated(IFBroSharpBrowser browser, IFBroSharpDownloadItem download_item, IFBroSharpDownloadItemCallback callback)
    {
        if (download_item.IsComplete())
        {
            Console.WriteLine($"下载完成: {download_item.GetFullPath()}");
        }
    }
}

3. 控制台日志收集

csharp
public class ConsoleLogger : BrowserEvent
{
    private readonly List<ConsoleMessage> consoleLogs = new List<ConsoleMessage>();

    public class ConsoleMessage
    {
        public DateTime Timestamp { get; set; }
        public FBroLogSeverityType Level { get; set; }
        public string Message { get; set; }
        public string Source { get; set; }
        public int Line { get; set; }
    }

    public override bool OnConsoleMessage(IFBroSharpBrowser browser, FBroLogSeverityType level, string message, string source, int line)
    {
        consoleLogs.Add(new ConsoleMessage
        {
            Timestamp = DateTime.Now,
            Level = level,
            Message = message,
            Source = source,
            Line = line
        });

        // 如果是错误日志,立即输出
        if (level == FBroLogSeverityType.LOGSEVERITY_ERROR)
        {
            Console.WriteLine($"[页面错误] {message} (来源: {source}:{line})");
        }

        return false;
    }

    public List<ConsoleMessage> GetLogs() => new List<ConsoleMessage>(consoleLogs);
    
    public void ClearLogs() => consoleLogs.Clear();
}

最佳实践

  1. 事件处理器复用:为不同用途的浏览器创建专门的事件处理器类
  2. 资源管理:在OnBeforeClose中及时清理资源,避免内存泄漏
  3. 错误处理:在关键事件中添加try-catch,避免异常导致程序崩溃
  4. 日志记录:合理使用Console.WriteLine或日志框架记录关键事件
  5. 异步操作:对于耗时操作,考虑使用异步处理避免阻塞UI
  6. user_flag管理:制定统一的标识命名规范,便于浏览器分类管理

user_flag最佳实践

命名规范建议

csharp
/// <summary>
/// user_flag命名规范示例
/// </summary>
public static class BrowserFlagNaming
{
    /// <summary>
    /// 生成标准格式的user_flag
    /// 格式:{模块}_{功能}_{时间戳}_{序号}
    /// </summary>
    /// <param name="module">模块名称</param>
    /// <param name="function">功能描述</param>
    /// <param name="index">序号(可选)</param>
    /// <returns>标准化的user_flag</returns>
    public static string GenerateStandardFlag(string module, string function, int? index = null)
    {
        string timestamp = DateTime.Now.ToString("yyyyMMddHHmmssffff");
        string indexSuffix = index.HasValue ? $"_{index:D3}" : "";
        return $"{module}_{function}_{timestamp}{indexSuffix}".ToLower();
    }

    /// <summary>
    /// 生成分类标识
    /// </summary>
    public static string GenerateCategoryFlag(string category, string subcategory, string identifier)
    {
        return $"{category}_{subcategory}_{identifier}_{DateTime.Now:yyyyMMddHHmmssfff}".ToLower();
    }

    /// <summary>
    /// 生成会话标识
    /// </summary>
    public static string GenerateSessionFlag(string sessionId, string purpose)
    {
        return $"session_{sessionId}_{purpose}_{DateTime.Now:yyyyMMddHHmmss}".ToLower();
    }
}

// 使用示例
string mainBrowserFlag = BrowserFlagNaming.GenerateStandardFlag("main", "search", 1);
// 结果: main_search_20241201143025000_001

string testBrowserFlag = BrowserFlagNaming.GenerateCategoryFlag("test", "ui", "login_form");
// 结果: test_ui_login_form_20241201143025000

string sessionFlag = BrowserFlagNaming.GenerateSessionFlag("user123", "shopping");
// 结果: session_user123_shopping_20241201143025

标识管理工具类

csharp
/// <summary>
/// 浏览器标识管理工具类
/// </summary>
public static class BrowserFlagManager
{
    private static readonly Dictionary<string, string> flagMappings = new Dictionary<string, string>();
    private static readonly object lockObject = new object();

    /// <summary>
    /// 注册浏览器标识映射
    /// </summary>
    /// <param name="logicalName">逻辑名称</param>
    /// <param name="actualFlag">实际标识</param>
    public static void RegisterFlag(string logicalName, string actualFlag)
    {
        lock (lockObject)
        {
            flagMappings[logicalName] = actualFlag;
        }
    }

    /// <summary>
    /// 获取浏览器(通过逻辑名称)
    /// </summary>
    /// <param name="logicalName">逻辑名称</param>
    /// <returns>浏览器实例</returns>
    public static FBroSharpBrowser GetBrowserByLogicalName(string logicalName)
    {
        lock (lockObject)
        {
            if (flagMappings.ContainsKey(logicalName))
            {
                string actualFlag = flagMappings[logicalName];
                return FBroSharpBrowserListControl.GetBrowserFromFlag(actualFlag);
            }
        }
        return null;
    }

    /// <summary>
    /// 移除标识映射
    /// </summary>
    /// <param name="logicalName">逻辑名称</param>
    public static void UnregisterFlag(string logicalName)
    {
        lock (lockObject)
        {
            flagMappings.Remove(logicalName);
        }
    }

    /// <summary>
    /// 验证标识唯一性
    /// </summary>
    /// <param name="flag">要验证的标识</param>
    /// <returns>是否唯一</returns>
    public static bool IsUnique(string flag)
    {
        var browser = FBroSharpBrowserListControl.GetBrowserFromFlag(flag);
        return browser == null;
    }

    /// <summary>
    /// 生成确保唯一的标识
    /// </summary>
    /// <param name="baseFlag">基础标识</param>
    /// <returns>唯一标识</returns>
    public static string EnsureUniqueFlag(string baseFlag)
    {
        if (IsUnique(baseFlag))
        {
            return baseFlag;
        }

        int counter = 1;
        string uniqueFlag;
        do
        {
            uniqueFlag = $"{baseFlag}_{counter:D3}";
            counter++;
        } while (!IsUnique(uniqueFlag) && counter < 1000);

        return uniqueFlag;
    }

    /// <summary>
    /// 获取所有活跃的标识
    /// </summary>
    /// <returns>标识列表</returns>
    public static List<string> GetAllActiveFlags()
    {
        lock (lockObject)
        {
            var activeFlags = new List<string>();
            foreach (var kvp in flagMappings)
            {
                var browser = FBroSharpBrowserListControl.GetBrowserFromFlag(kvp.Value);
                if (browser != null && browser.IsValid())
                {
                    activeFlags.Add(kvp.Value);
                }
            }
            return activeFlags;
        }
    }
}

标识验证和监控

csharp
/// <summary>
/// 浏览器标识验证器
/// </summary>
public static class BrowserFlagValidator
{
    /// <summary>
    /// 验证标识格式
    /// </summary>
    /// <param name="flag">要验证的标识</param>
    /// <returns>验证结果</returns>
    public static ValidationResult ValidateFlag(string flag)
    {
        var result = new ValidationResult { IsValid = true, Flag = flag };

        if (string.IsNullOrWhiteSpace(flag))
        {
            result.IsValid = false;
            result.Errors.Add("标识不能为空");
            return result;
        }

        // 长度检查
        if (flag.Length > 100)
        {
            result.IsValid = false;
            result.Errors.Add("标识长度不能超过100个字符");
        }

        // 字符检查
        if (!System.Text.RegularExpressions.Regex.IsMatch(flag, @"^[a-zA-Z0-9_\-]+$"))
        {
            result.IsValid = false;
            result.Errors.Add("标识只能包含字母、数字、下划线和连字符");
        }

        // 格式建议检查
        if (!flag.Contains("_"))
        {
            result.Warnings.Add("建议使用下划线分隔标识的不同部分");
        }

        // 时间戳检查
        if (!System.Text.RegularExpressions.Regex.IsMatch(flag, @"\d{14,}"))
        {
            result.Warnings.Add("建议在标识中包含时间戳以确保唯一性");
        }

        return result;
    }

    public class ValidationResult
    {
        public bool IsValid { get; set; }
        public string Flag { get; set; }
        public List<string> Errors { get; set; } = new List<string>();
        public List<string> Warnings { get; set; } = new List<string>();

        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.AppendLine($"标识: {Flag}");
            sb.AppendLine($"验证状态: {(IsValid ? "通过" : "失败")}");
            
            if (Errors.Any())
            {
                sb.AppendLine("错误:");
                foreach (var error in Errors)
                {
                    sb.AppendLine($"  - {error}");
                }
            }

            if (Warnings.Any())
            {
                sb.AppendLine("警告:");
                foreach (var warning in Warnings)
                {
                    sb.AppendLine($"  - {warning}");
                }
            }

            return sb.ToString();
        }
    }
}

使用示例和最佳实践总结

csharp
/// <summary>
/// user_flag最佳实践示例
/// </summary>
public class BestPracticeExample
{
    /// <summary>
    /// 推荐的浏览器创建方式
    /// </summary>
    public void RecommendedBrowserCreation()
    {
        // 1. 生成标准化标识
        string baseFlag = BrowserFlagNaming.GenerateStandardFlag("ecommerce", "product_view", 1);
        
        // 2. 验证标识格式
        var validation = BrowserFlagValidator.ValidateFlag(baseFlag);
        if (!validation.IsValid)
        {
            Console.WriteLine($"标识验证失败: {validation}");
            return;
        }

        // 3. 确保唯一性
        string uniqueFlag = BrowserFlagManager.EnsureUniqueFlag(baseFlag);

        // 4. 注册逻辑名称映射
        BrowserFlagManager.RegisterFlag("主要商品浏览器", uniqueFlag);

        try
        {
            // 5. 创建浏览器
            FBroSharpControl.CreatBrowser(
                "https://www.example-shop.com/products",
                CreateWindowInfo(),
                default,
                default,
                CreateExtraInfo(uniqueFlag),
                new BrowserEvent(),
                default,
                uniqueFlag
            );

            Console.WriteLine($"浏览器创建成功,标识: {uniqueFlag}");
        }
        catch (Exception ex)
        {
            // 6. 错误时清理映射
            BrowserFlagManager.UnregisterFlag("主要商品浏览器");
            Console.WriteLine($"浏览器创建失败: {ex.Message}");
        }
    }

    /// <summary>
    /// 获取浏览器的推荐方式
    /// </summary>
    public void RecommendedBrowserRetrieval()
    {
        // 方式1:通过逻辑名称获取
        var browser1 = BrowserFlagManager.GetBrowserByLogicalName("主要商品浏览器");
        
        // 方式2:通过完整标识获取
        var browser2 = FBroSharpBrowserListControl.GetBrowserFromFlag("ecommerce_product_view_20241201143025000_001");
        
        // 方式3:验证浏览器有效性
        if (browser1 != null && browser1.IsValid())
        {
            // 安全地使用浏览器
            browser1.GetMainFrame().LoadURL("https://new-url.com");
        }
    }

    private FBroSharpWindowsInfo CreateWindowInfo()
    {
        return new FBroSharpWindowsInfo
        {
            parent_window = IntPtr.Zero,
            x = 100,
            y = 100,
            width = 1200,
            height = 800,
            window_name = "商品浏览器"
        };
    }

    private FBroSharpDictionaryValue CreateExtraInfo(string flag)
    {
        var extraInfo = FBroSharpDictionaryValue.Create();
        extraInfo.SetString("user_flag", flag);
        extraInfo.SetString("creation_time", DateTime.Now.ToString());
        extraInfo.SetString("purpose", "产品浏览");
        return extraInfo;
    }
}

注意事项

  1. 线程安全:事件回调可能在不同线程中执行,注意线程安全
  2. 生命周期管理:确保在浏览器关闭前清理所有相关资源
  3. 回调对象释放:及时调用callback.Dispose()释放回调对象
  4. 避免循环引用:事件处理器不要持有浏览器的强引用
  5. user_flag唯一性:确保user_flag在应用程序范围内唯一,避免冲突
  6. 标识生命周期:浏览器关闭后及时清理相关的标识映射和引用
  7. 标识长度限制:避免使用过长的user_flag,建议控制在100字符以内
  8. 特殊字符避免:user_flag中避免使用特殊字符,推荐使用字母、数字、下划线

相关文档

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