V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
skylord
V2EX  ›  Windows

在拖拽文件还没有进入任何窗口时,怎样获取鼠标拖拽的文件信息?

  •  2
     
  •   skylord · 8 天前 · 2502 次点击

    像豆包电脑那样,鼠标在桌面上拖动一个文件,刚开始拖动,还没进入任何窗口,豆包就能感知到,然后在屏幕右下角弹出提示,可以帮忙解读文件。这个是怎么实现的? 目前,通过鼠标钩子判断是否在拖拽,如果在拖拽,就通过 OleGetClipboard 函数从剪贴板获取文件信息,但是剪贴板里面没有文件相关的信息。代码如下:

    #include <windows.h>
    #include <shlobj.h>
    #include <vector>
    #include <string>
    #include <iostream>
    #include <chrono>
    #include <thread>
    
    HHOOK g_mouseHook;
    bool g_isDragging = false;
    POINT g_dragStartPos = { 0, 0 };
    const int DRAG_THRESHOLD = 3; // 拖动阈值(像素)
    
    void ExtractFileInfoFromDropClipboard();
    void CheckOtherDataFormats(IDataObject* pDataObject);
    void ExtractFileTypeInfo(const std::wstring& filePath);
    
    
    LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
        if (nCode == HC_ACTION) {
            MSLLHOOKSTRUCT* pMouse = (MSLLHOOKSTRUCT*)lParam;
    
            if (wParam == WM_LBUTTONDOWN) {
                //std::cout << "Mouse Button Down at (" << pMouse->pt.x << ", " << pMouse->pt.y << ")\n";
                // 你可以在这里检测是否是拖拽开始的条件
                // 记录拖动起始位置
                g_dragStartPos = pMouse->pt;
                g_isDragging = false;
            }
    
            if (wParam == WM_MOUSEMOVE) {
                // 检测鼠标是否有拖拽操作
                if (GetAsyncKeyState(VK_LBUTTON) & 0x8000) {
                    // 检查是否超过拖动阈值
                    int deltaX = abs(pMouse->pt.x - g_dragStartPos.x);
                    int deltaY = abs(pMouse->pt.y - g_dragStartPos.y);
    
                    if (!g_isDragging && (deltaX > DRAG_THRESHOLD || deltaY > DRAG_THRESHOLD)) {
                        g_isDragging = true;
                        std::cout << "move begin..." << std::endl;
    
                        // 尝试从拖放剪贴板获取文件信息
                        ExtractFileInfoFromDropClipboard();
    
                    }
                }
            }
    
            if (wParam == WM_LBUTTONUP)
            {
                if (g_isDragging) {
                    std::cout << "move end" << std::endl;
                    g_isDragging = false;
                }
            }
        }
        return CallNextHookEx(g_mouseHook, nCode, wParam, lParam);
    }
    
    // 从拖放剪贴板提取文件信息
    void ExtractFileInfoFromDropClipboard() {
        HRESULT hr = OleInitialize(nullptr);
        if (FAILED(hr)) {
            std::cerr << "OleInitialize failed: " << hr << std::endl;
            return;
        }
        IDataObject* pDataObject = nullptr;
        // 获取拖放剪贴板数据
        hr = OleGetClipboard(&pDataObject);
        if (SUCCEEDED(hr) && pDataObject) {
            FORMATETC fmtetc = {
                CF_HDROP,
                NULL,
                DVASPECT_CONTENT,
                -1,
                TYMED_HGLOBAL
            };
    
            if (SUCCEEDED(pDataObject->QueryGetData(&fmtetc))) {
                STGMEDIUM stgmed;
                // 查询 HDROP 数据
                hr = pDataObject->GetData(&fmtetc, &stgmed);
                if (SUCCEEDED(hr)) {
                    HDROP hDrop = static_cast<HDROP>(GlobalLock(stgmed.hGlobal));
                    if (hDrop) {
                        // 获取文件数量
                        UINT fileCount = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
                        std::cout << "drag file count: " << fileCount << std::endl;
    
                        // 遍历所有文件
                        for (UINT i = 0; i < fileCount; i++) {
                            // 获取文件路径长度
                            UINT pathLength = DragQueryFile(hDrop, i, nullptr, 0);
                            if (pathLength > 0) {
                                std::vector<TCHAR> buffer(pathLength + 1);
                                DragQueryFile(hDrop, i, buffer.data(), pathLength + 1);
    
                                std::wstring filePath(buffer.data());
                                std::wcout << L"file " << (i + 1) << L": " << filePath << std::endl;
    
                                // 获取文件属性信息
                                ExtractFileTypeInfo(filePath);
                            }
                        }
    
                        GlobalUnlock(stgmed.hGlobal);
                    }
                    ReleaseStgMedium(&stgmed);
                }
            }
            else
            {
                std::wcout << L"CF_HDROP format not supported" << std::endl;
                // 尝试其他数据格式
                //CheckOtherDataFormats(pDataObject);
            }
            pDataObject->Release();
        }
        else {
            std::wcerr << L"can not get clipboard data" << std::endl;
        }
        OleUninitialize();
    }
    
    // 检查其他数据格式
    void CheckOtherDataFormats(IDataObject* pDataObject) {
        // 查询支持的数据格式
        IEnumFORMATETC* pEnumFormat = nullptr;
        HRESULT hr = pDataObject->EnumFormatEtc(DATADIR_GET, &pEnumFormat);
    
        if (SUCCEEDED(hr) && pEnumFormat) {
            FORMATETC fmtetc;
            ULONG fetched = 0;
    
            std::cout << "data format in clipboard:" << std::endl;
    
            while (pEnumFormat->Next(1, &fmtetc, &fetched) == S_OK && fetched == 1) {
                TCHAR formatName[256];
                if (GetClipboardFormatName(fmtetc.cfFormat, formatName, 256) > 0) {
                    std::wcout << L"format1: " << formatName << L" (ID: " << fmtetc.cfFormat << L")" << std::endl;
                }
                else {
                    // 标准格式
                    std::string stdFormatName;
                    switch (fmtetc.cfFormat) {
                    case CF_TEXT: stdFormatName = "CF_TEXT"; break;
                    case CF_UNICODETEXT: stdFormatName = "CF_UNICODETEXT"; break;
                    case CF_BITMAP: stdFormatName = "CF_BITMAP"; break;
                    case CF_DIB: stdFormatName = "CF_DIB"; break;
                    case CF_HDROP: stdFormatName = "CF_HDROP"; break;
                    case CF_LOCALE: stdFormatName = "CF_LOCALE"; break;
                    case CF_OEMTEXT: stdFormatName = "CF_OEMTEXT"; break;
                    default: stdFormatName = "unknown"; break;
                    }
                    std::cout << "format2: " << stdFormatName << " (ID: " << fmtetc.cfFormat << ")" << std::endl;
                }
            }
    
            pEnumFormat->Release();
        }
    }
    
    // 提取文件类型信息
    void ExtractFileTypeInfo(const std::wstring& filePath) {
        // 获取文件属性
        DWORD attr = GetFileAttributes(filePath.c_str());
        if (attr != INVALID_FILE_ATTRIBUTES) {
            if (attr & FILE_ATTRIBUTE_DIRECTORY) {
                std::wcout << L"  type: directory" << std::endl;
            }
            else {
                std::wcout << L"  type: file" << std::endl;
    
                // 获取文件扩展名
                size_t dotPos = filePath.find_last_of(L'.');
                if (dotPos != std::wstring::npos) {
                    std::wstring extension = filePath.substr(dotPos);
                    std::wcout << L"  extension: " << extension << std::endl;
                }
    
                // 获取文件大小
                HANDLE hFile = CreateFile(filePath.c_str(), GENERIC_READ, FILE_SHARE_READ,
                    nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
                if (hFile != INVALID_HANDLE_VALUE) {
                    LARGE_INTEGER fileSize;
                    if (GetFileSizeEx(hFile, &fileSize)) {
                        std::wcout << L"  size: " << fileSize.QuadPart << L" bytes" << std::endl;
                    }
                    CloseHandle(hFile);
                }
            }
    
            // 显示文件属性
            std::wcout << L"  properties: ";
            if (attr & FILE_ATTRIBUTE_READONLY) std::wcout << L"[read-only]";
            if (attr & FILE_ATTRIBUTE_HIDDEN) std::wcout << L"[hidden]";
            if (attr & FILE_ATTRIBUTE_SYSTEM) std::wcout << L"[system]";
            if (attr & FILE_ATTRIBUTE_ARCHIVE) std::wcout << L"[archive]";
            std::wcout << std::endl;
        }
    }
    
    void InstallMouseHook() {
        g_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, NULL, 0);
        if (g_mouseHook == NULL) {
            std::cerr << "Failed to install mouse hook!" << std::endl;
        }
    }
    
    int main() {
        InstallMouseHook();
    
        // 进入消息循环
        MSG msg;
        while (GetMessage(&msg, NULL, 0, 0)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    
        // 卸载钩子
        UnhookWindowsHookEx(g_mouseHook);
        return 0;
    }
    
    31 条回复    2025-11-30 10:00:06 +08:00
    BBrother
        1
    BBrother  
       8 天前
    创建一个全屏的透明窗口试试?
    huihushijie1996
        2
    huihushijie1996  
       8 天前
    桌面也是一个窗口
    huihushijie1996
        3
    huihushijie1996  
       8 天前
    @huihushijie1996 只需要截获桌面窗口的消息即可
    skylord
        4
    skylord  
    OP
       8 天前
    在 windows 资源管理器里拖动也能截获吗?
    skylord
        5
    skylord  
    OP
       8 天前
    @BBrother 窗口被遮挡住就不行了
    zjsxwc
        6
    zjsxwc  
       8 天前
    问下 ai ,就有代码:用 python 实现:捕获系统级的鼠标操作与文件拖拽
    若要获取非自身窗口的鼠标点击或文件拖拽(如系统全局范围),不能通过 DWM 实现,需借助鼠标钩子(如 SetWindowsHookEx 函数设置 WH_MOUSE_LL 钩子)捕获全局鼠标事件。而获取全局文件拖拽时,可结合 Shell 扩展编程或钩子监听资源管理器的文件选择操作,通过获取 SysListView32 控件(资源管理器文件列表控件)的句柄,进一步查询拖拽文件的相关信息,但这种方式需注意权限和系统兼容性问题。
    worldhandsomeboy
        7
    worldhandsomeboy  
       8 天前
    鼠标相关的 api 有一些属于焦点的函数,在你左击文件拖拽的时候,焦点所在的文件就是一个对象,包括路径、类型这些。
    skylord
        8
    skylord  
    OP
       8 天前
    @zjsxwc ai 提这些都试过了,注册 shell 扩展,鼠标钩子,dll 注入都有问题,没法实现
    sir283
        9
    sir283  
       8 天前
    你要用管理员权限运行你的软件啊,豆包都是用管理员权限 hook 的。
    skylord
        10
    skylord  
    OP
       8 天前   ❤️ 1
    @sir283 豆包没有管理员权限,试过很多次了
    guanzhangzhang
        11
    guanzhangzhang  
       8 天前
    qq 前不久更新后也有这种感知,然后我在设置里关闭了
    skylord
        12
    skylord  
    OP
       8 天前
    @guanzhangzhang 我们公司开发了个智能助手,也要这个功能😂
    momo1999
        13
    momo1999  
       8 天前
    拖拽应该是个单独的结构,不影响剪贴板吧,要不然我拖个文件,我之前复制的东西就没了?
    skylord
        14
    skylord  
    OP
       8 天前
    @momo1999 不是同一个剪贴板,有两个剪贴板,一个 ole 剪贴板(给文件拖拽用的),一个系统剪贴板(给复制粘贴用),拖拽不会影响系统剪贴板
    momo1999
        15
    momo1999  
       8 天前
    @skylord 看了一下资源管理器进程里面多了豆包的 dll ,路径在%temp%\doubao_ext\1.81.7\shellext.dll
    guanzhangzhang
        17
    guanzhangzhang  
       8 天前
    @skylord #12 看看有啥类似 processmonitor 啥的看看能不能监听到 qq 的这块大概 dll ,看看符号表啥的
    nnnnnnamgn
        18
    nnnnnnamgn  
       8 天前
    豆包那个实现应该很复杂,不如搞一个类似 360 加速器那样的小的悬浮窗,让用户自己拖拽文件进去
    skylord
        19
    skylord  
    OP
       8 天前
    @ljpCN 感谢,完整读了一遍,没发现什么线索
    skylord
        20
    skylord  
    OP
       8 天前
    @bluearc 不满足需求啊🤣
    skylord
        21
    skylord  
    OP
       8 天前
    @momo1999 这个是个 ContextMenuHandlers,跟右键菜单相关的
    skylord
        22
    skylord  
    OP
       8 天前
    @worldhandsomeboy 能详细说说吗?
    worldhandsomeboy
        23
    worldhandsomeboy  
       8 天前
    创建了个 demo 监听粘贴板,在拖动桌面文件过程中很多时候没有写入剪贴板,有时候写入了但复现不出来。
    下载了 window 的豆包,没看到有这个功能。
    dode
        24
    dode  
       8 天前
    可能是监控鼠标位置
    skylord
        25
    skylord  
    OP
       8 天前
    @dode 通过鼠标钩子监控鼠标位置,但是拿不到文件信息
    skylord
        26
    skylord  
    OP
       8 天前
    @worldhandsomeboy 功能是有的,你在拖动 doc ,txt 等文档类的文件时,右下角会弹出一个 tips ,告诉你它可以帮你解读文件
    nilaoda
        27
    nilaoda  
       8 天前   ❤️ 4
    问了 Gemini 3 Pro Preview ,可以使用 UIAutomationClient 监听 SysDragImage 元素来实现。

    整个流程如下:
    1. 利用 UI Automation 监听 SysDragImage 出现以捕获拖拽行为
    2. 结合鼠标坐标定位与 Shell COM 接口预先读取并校验选中文件的类型
    3. 只有当文件类型符合要求时才弹出悬浮窗,且窗口的关闭逻辑完全依赖鼠标按键状态监测,实现精准触发

    .NET 调用比较方便,让他写了一个 WPF 的 demo ,不知道你要的是不是这种效果?



    https://gist.github.com/nilaoda/26b5f32bb383a3285cbe89434c30a232

    让 AI 用 C/C++实现一下就好。

    BTW ,可以研究研究 win11 顶部分享框的实现……
    ysc3839
        28
    ysc3839  
       7 天前 via Android
    不知道 SetWinEventHook 的 EVENT_OBJECT_DRAGSTART 等事件是否可行?
    https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook
    skylord
        29
    skylord  
    OP
       7 天前
    @nilaoda 感谢,我试试
    skylord
        30
    skylord  
    OP
       7 天前
    @ysc3839 感谢,我看一下这个文档
    skylord
        31
    skylord  
    OP
       6 天前
    @nilaoda 这个方式确实是可以的
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   878 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 19:48 · PVG 03:48 · LAX 11:48 · JFK 14:48
    ♥ Do have faith in what you're doing.