这是一个非常经典的技术,通常用于实现自动化测试、数据抓取或需要与旧系统集成等场景,ATL 之所以适合,因为它提供了轻量级、高性能的 COM 组件开发框架,非常适合创建与浏览器交互的客户端。

atl 模仿网页登录客户端
(图片来源网络,侵删)

核心思路是:创建一个 ATL COM 组件,这个组件内部使用 Microsoft Web Browser 控件 (实际上是 MSHTML 和 IE 的封装) 来嵌入一个浏览器实例,通过这个组件的接口,我们可以编程方式控制浏览器,在网页的登录框中输入用户名、密码,并点击登录按钮。


核心原理:ATL + Web Browser 控件

  1. ATL COM 服务器: 我们将创建一个 ATL DLL 项目,它导出一个 COM 类,这个 COM 类就是我们客户端的“大脑”。
  2. Web Browser 控件: 这不是一个 ATL 特有的控件,而是 Windows 平台自带的基于 IE 的 ActiveX 控件,我们可以在 ATL 的对话框或窗口中嵌入它。
  3. 自动化控制: Web Browser 控件提供了丰富的自动化接口 (主要通过 IWebBrowser2HTMLDocument 接口),允许外部程序(我们的 ATL 组件)访问和操作其内部的网页内容,如获取输入框、修改其值、模拟点击事件等。

详细步骤:创建 ATL 登录客户端

我们将分步完成这个项目。

第一步:创建 ATL 项目

  1. 打开 Visual Studio。
  2. 创建新项目 -> 选择 ATL 项目模板。
  3. 给项目命名,AtlWebLoginClient
  4. 在 ATL 项目向导中,保持默认设置即可,点击“完成”。

第二步:添加一个支持对话框的 ATL 对象

我们需要一个窗口来承载 Web Browser 控件。

  1. 解决方案资源管理器 中右键点击你的项目,选择 添加 -> 添加类
  2. 选择 ATL 支持对话框的 ATL 对象,点击“添加”。
  3. 设置类名,LoginDlg
  4. 在“选项”页面中,确保 “支持窗口less” 没有被勾选,我们需要一个有窗口的对话框来嵌入浏览器。
  5. 点击“完成”。

第三步:在对话框中添加 Web Browser 控件

  1. 打开 LoginDlg.h 文件。

    atl 模仿网页登录客户端
    (图片来源网络,侵删)
  2. 找到 LoginDlg 类的声明,添加一个 IWebBrowser2 类型的成员变量,用于控制浏览器。

    // LoginDlg.h
    #pragma once
    // ... 其他 include ...
    #include <exdispid.h> // 包含 DISPID 常量
    class ATL_NO_VTABLE CLoginDlg :
        public CComObjectRootEx<CComSingleThreadModel>,
        public CComCoClass<CLoginDlg, &CLSID_LoginDlg>,
        public IDialogImpl<CLoginDlg>,
        public IConnectionPointContainerImpl<CLoginDlg>,
        public IDispEventImpl<1, CLoginDlg, &DIID_DWebBrowserEvents2, &LIBID_ATLWEBLOGINCLIENTLib>, // 用于接收浏览器事件
        public IObjectWithSiteImpl<CLoginDlg> // 用于让浏览器知道我们的站点
    {
    // ... 其他代码 ...
    public:
        // 添加 WebBrowser 控件变量
        CComPtr<IWebBrowser2> m_spWebBrowser;
    // ... 其他代码 ...
    };
  3. 打开 LoginDlg.rgs 文件(这是注册脚本,用于定义控件的布局),在 Dialog 节点下,添加 WebBrowser 控件。

    HKCR
    {
        // ... 其他 ATL 注册信息 ...
        NoRemove CLSID
        {
            // ... 其他 CLSID ...
            ForceRemove {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} = s 'LoginDlg'
            {
                InprocServer32 = s '%MODULE%'
                {
                    val ThreadingModel = s 'Apartment'
                }
                // 添加 WebBrowser 控件
                Control
                {
                    clsid = s '{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}';
                    // 这是 WebBrowser 控件的 CLSID
                }
            }
        }
    }
  4. 我们需要在对话框的代码中初始化这个控件,打开 LoginDlg.cpp,修改 LoginDlg 构造函数和 OnInitDialog 方法。

    // LoginDlg.cpp
    #include "stdafx.h"
    #include "LoginDlg.h"
    #include "mshtml.h" // 包含 HTML DOM 接口
    // ... 其他代码 ...
    CLoginDlg::CLoginDlg()
    {
    }
    // ... 其他代码 ...
    LRESULT CLoginDlg::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
    {
        // 初始化 WebBrowser 控件
        CComPtr<IUnknown> spUnk;
        if (SUCCEEDED(GetDlgItem(IDC_WEBBROWSER, &spUnk)))
        {
            spUnk->QueryInterface(IID_IWebBrowser2, (void**)&m_spWebBrowser);
            if (m_spWebBrowser)
            {
                // 设置我们的对象为浏览器控件的站点
                IObjectWithSite* pSite = NULL;
                if (SUCCEEDED(QueryInterface(IID_IObjectWithSite, (void**)&pSite)))
                {
                    pSite->SetSite((IServiceProvider*)this);
                    pSite->Release();
                }
                // 导航到目标登录页面
                m_spWebBrowser->Navigate(CComBSTR(L"https://example.com/login"), NULL, NULL, NULL, NULL);
            }
        }
        return TRUE;
    }

    注意:上面的 IDC_WEBBROWSER 是对话框模板中 WebBrowser 控件的 ID,你可能需要在 LoginDlg.rc 中手动添加一个对话框资源,并从工具箱拖拽一个 "Web Browser" 控件到对话框上,然后为其设置 IDC_WEBBROWSER

第四步:实现自动化登录逻辑

这是最关键的一步,我们需要在页面加载完成后,找到用户名、密码输入框和登录按钮,并操作它们。

  1. 处理网页加载完成事件:为了确保在操作 DOM 之前页面已经完全加载,我们需要处理 DocumentComplete 事件,这需要我们实现 IDispEventImpl 接口。

    • LoginDlg.h 中,我们已经声明了 IDispEventImpl
    • LoginDlg.cpp 中,添加事件映射和事件处理函数。
    // LoginDlg.cpp
    #include "stdafx.h"
    #include "LoginDlg.h"
    #include "mshtml.h"
    // ... 其他代码 ...
    BEGIN_SINK_MAP(CLoginDlg)
        SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, OnDocumentComplete)
    END_SINK_MAP()
    void STDMETHODCALLTYPE CLoginDlg::OnDocumentComplete(IDispatch* pDisp, VARIANT* URL)
    {
        // 检查事件是否来自我们的顶级浏览器窗口
        CComQIPtr<IWebBrowser2> spBrowser = pDisp;
        if (spBrowser && m_spBrowser == spBrowser)
        {
            // 页面加载完成,可以执行登录操作
            PerformLogin();
        }
    }
    void CLoginDlg::PerformLogin()
    {
        if (!m_spWebBrowser)
            return;
        CComPtr<IDispatch> spDispDoc;
        m_spWebBrowser->get_Document(&spDispDoc);
        if (!spDispDoc)
            return;
        CComQIPtr<IHTMLDocument2> spHtmlDoc = spDispDoc;
        if (!spHtmlDoc)
            return;
        CComPtr<IHTMLElementCollection> spCollInputs;
        spHtmlDoc->get_all(&spCollInputs);
        if (!spCollInputs)
            return;
        // 1. 查找用户名输入框 (假设其 id 为 "username")
        CComVariant varUsernameId(L"username");
        CComPtr<IHTMLElement> spUsernameInput;
        if (SUCCEEDED(spCollInputs->item(varUsernameId, CComVariant(), &spUsernameInput)))
        {
            CComQIPtr<IHTMLInputElement> spUsernameInputElem = spUsernameInput;
            if (spUsernameInputElem)
            {
                spUsernameInputElem->put_value(CComBSTR(L"your_username"));
            }
        }
        // 2. 查找密码输入框 (假设其 id 为 "password")
        CComVariant varPasswordId(L"password");
        CComPtr<IHTMLElement> spPasswordInput;
        if (SUCCEEDED(spCollInputs->item(varPasswordId, CComVariant(), &spPasswordInput)))
        {
            CComQIPtr<IHTMLInputElement> spPasswordInputElem = spPasswordInput;
            if (spPasswordInputElem)
            {
                spPasswordInputElem->put_value(CComBSTR(L"your_password"));
            }
        }
        // 3. 查找登录按钮 (假设其 id 为 "login-btn")
        CComVariant varLoginButtonId(L"login-btn");
        CComPtr<IHTMLElement> spLoginButton;
        if (SUCCEEDED(spCollInputs->item(varLoginButtonId, CComVariant(), &spLoginButton)))
        {
            // 模拟点击事件
            spLoginButton->click();
        }
    }

    注意:your_username, your_password, username, password, login-btn 这些都是根据你要登录的实际网页 HTML 结构来确定的,你需要使用浏览器的“开发者工具” (F12) 来检查这些元素的 ID、Name 或其他属性。

第五步:编译和测试

  1. 编译项目:确保没有错误。

  2. 注册 COM 组件:在项目属性的“生成事件”->“后期生成事件”->“命令行”中,添加 regsvr32 /s "$(TargetPath)"

  3. 创建测试程序

    • 创建一个新的简单的 MFC 对话框应用程序项目,TestClient
    • 在主对话框中,添加一个按钮,启动登录”。
    • 为该按钮添加点击事件处理函数。
    • 在函数中,使用 CoInitialize 初始化 COM,然后创建我们的 LoginDlg COM 对象并显示其对话框。
    // TestDlg.cpp
    #include "stdafx.h"
    #include "TestDlg.h"
    #include "..\AtlWebLoginClient\AtlWebLoginClient_i.h" // 引入我们的 ATL 组件头文件
    // ...
    void CTestDlg::OnBnClickedButtonStart()
    {
        CoInitialize(NULL);
        CLSID clsid;
        CLSIDFromProgID(L"AtlWebLoginLoginDlg", &clsid);
        CComPtr<IUnknown> spUnk;
        if (SUCCEEDED(CoCreateInstance(clsid, NULL, CLSCTX_ALL, IID_IUnknown, (void**)&spUnk)))
        {
            CComQIPtr<IDialogImpl> spDlg = spUnk;
            if (spDlg)
            {
                spDlg->DoModal();
            }
        }
        CoUninitialize();
    }
  4. 运行 TestClient,点击“启动登录”按钮,你应该能看到一个对话框弹出,并自动导航到指定网址,然后自动填写并提交登录表单。


高级主题和注意事项

  1. 错误处理:上述代码中的 SUCCEEDED 检查是基础,在实际应用中,需要更健壮的错误处理,例如当找不到某个元素时,不应直接崩溃,而应给出提示或重试。
  2. 等待机制OnDocumentComplete 事件可能因页面中的框架(iframe)而多次触发,上面的代码通过比较 pDispm_spWebBrowser 来确保只在主窗口加载完成时执行一次,有时,页面加载是异步的(AJAX),可能需要更复杂的等待逻辑,例如轮询某个元素是否存在。
  3. 安全性
    • 不要硬编码密码:应从安全配置文件或用户输入中获取。
    • HTTPS:确保登录网站使用 HTTPS,以防止密码在传输过程中被窃听。
    • 现代网站的挑战:许多现代网站使用复杂的反爬虫机制、动态加载内容或前端框架(如 React, Vue),这使得传统的 DOM 操作变得困难,它们可能需要你处理更复杂的认证流程,如 token 交换、处理 JavaScript 沙箱等。
  4. 替代方案:对于更现代、更复杂的自动化任务,可以考虑使用 SeleniumPlaywright,它们提供了更高级的 API 和对现代浏览器的更好支持(通过 WebDriver 协议),并且支持 Chrome, Firefox, Edge 等,ATL + WebBrowser 的方法在处理基于传统 HTML 的网站时依然非常有效,尤其是在需要与现有 Windows C++ 代码集成的场景下。

这个完整的例子展示了如何利用 ATL 的强大功能来创建一个功能性的网页登录客户端,关键在于理解 COM 交互和 HTML DOM 操作的结合。