65.9K
CodeProject 正在变化。 阅读更多。
Home

将文件名发送到剪贴板 - 创建一个不使用 CRT 的微型工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (25投票s)

2009年8月29日

CPOL

6分钟阅读

viewsIcon

42452

downloadIcon

323

Name2Clip 位于 Windows 资源管理器的“发送到”菜单中,并将选定的文件名发送到系统剪贴板。

Name2Clip 工具的快捷方式放置在 Windows 资源管理器的“发送到”菜单中

A shortcut to Name2Clip tool

目录

引言

众所周知,从 Windows 资源管理器复制文件的完整路径到剪贴板可能非常繁琐。Name2Clip 的主要目标就是解决这个问题。Name2Clip 是一个微小实用程序,它将命令行参数发送到系统剪贴板。Name2Clip 可以用于处理任意数量的字符串作为命令行参数。该实用程序并不关心字符串的内容。它只是将字符串作为文本复制到系统剪贴板,每个字符串占一行。本文介绍了如何将 Windows 资源管理器的“发送到”上下文菜单与 Name2Clip 结合使用,以便将选定文件和文件夹的完整路径复制到剪贴板。

为了保持事物的尽可能小巧轻便,Name2Clip 不使用任何外部框架或库。它甚至不使用 C 运行时库 (CRT)。Name2Clip 是用 C 编程语言实现的,并且严格调用 Windows API 函数。因此,Name2Clip.exe 速度极快,大小仅约 7 KB。

用法

Name2Clip 可以在从 Windows 2000 及以上版本的任何 Windows 系统上运行。无需安装。以下是用法摘要

Name2Clip [/c] [/s] [/n] params
   /c - Coding friendly format: '\' replaced by '\\'.
   /s - Paths are converted to MS-DOS 8.3 format.
   /n - Only names rather than full paths are copied.

当不带参数启动时,Name2Clip 会显示其用法。

除了命令行开关外,Name2Clip 在启动时还会检查键盘状态。因此,如果按下某个键,Name2Clip 会检测到它并表现得如同指定了相应的命令行开关。以下是支持的按键列表

  • 按下了 **Ctrl** 键(等同于 /c 开关) - 会插入双反斜杠。示例:C:\\Program Files\\Windows NT\\Accessories\\wordpad.exe
  • 按下了 **Shift** 键(等同于 /s 开关) - 会将 MS-DOS 8.3 名称放入剪贴板。示例:C:\PROGRA~1\WINDOW~2\ACCESS~1\wordpad.exe
  • 同时按下了 **Ctrl+Shift** 键 - 以上两者的组合。示例:C:\\PROGRA~1\\WINDOW~2\\ACCESS~1\\wordpad.exe

感谢 Nick Carruthers 和他的文章 Copy Path Context Menu Extension,提供了关于这些按键的灵感。

如何使用 Name2Clip 的“发送到”上下文菜单

Windows 资源管理器的“发送到”菜单是从特殊的 SendTo 文件夹的内容创建的。该文件夹的位置如下

  • Windows XP%USERPROFILE%\SendTo,其中 %USERPROFILE% 通常展开为:C:\Documents and Settings\{username}
  • Windows VistaWindows 7%APPDATA%\Microsoft\Windows\SendTo,其中 %APPDATA% 通常展开为:C:\Users\{username}\AppData\Roaming

当链接放置在 SendTo 文件夹中时,该链接的名称将显示在 Windows 资源管理器的“发送到”上下文菜单中。当用户从“发送到”菜单中选择链接时,当前选定文件和文件夹的完整路径将作为命令行参数传递给链接中指定的程序。我们所要做的就是将 Name2Clip 的链接放置到 SendTo 文件夹中。

以下是有关如何将项目添加到“发送到”菜单的详细说明:Add an item to the Send To menu。该文章适用于 Windows XP,但对于任何 Windows 平台,过程基本相同。只有 SendTo 文件夹的位置不同。

背景

那么,为什么要开发一个不使用 CRT 的应用程序呢?这里有几个原因

  • 独立性。一个不使用 CRT 的应用程序几乎完全独立于开发环境。可以使用任何合适的 C 编译器。或者,为了达到类似的独立性水平,程序可以与 CRT 进行静态链接。但是,这会增加程序的体积。
  • 最小的可能尺寸。通过仅使用 Windows API 函数,程序可以在 Windows 平台上实现最小的可能尺寸。为了使其更小,可能需要使用汇编器作为开发工具。但我们还是现实一点。
  • 很有趣。很可能,您已经发现上述几点都无关紧要了。如今,没有人会理智地在没有现代工具的情况下开始进行严肃的开发。当然,这包括 CRT 支持。然而,Name2Clip 如此小巧简单,不使用 CRT 进行开发可能很有趣。这是此次练习的真正原因。

有时,不使用 CRT 开发程序是有正当理由的。通常是设备驱动程序、在嵌入式环境中运行的代码以及其他接近硬件的应用程序。常规的桌面应用程序不应如此开发。实际上,现代桌面应用程序根本就不应该用原生语言开发。

以下是 Name2Clip 为摆脱 CRT 而付出的代价

  • 不进行堆栈帧运行时错误检查
    • 检测局部变量(如数组)的溢出和欠溢。
    • 堆栈指针验证,可检测堆栈指针损坏。
  • 完全没有异常处理。任何尝试使用 __try__except__finally 块的都会导致未解析的外部链接器错误。异常处理机制与 CRT 紧密耦合。
  • 没有针对尺寸的优化。编译器通常会将许多语言结构替换为 CRT 例程。例如,这段代码
    int arr[10] = { 0 };

    将无法链接,因为编译器会在后台插入对 memset 函数的调用来将数组初始化为零。

  • 没有 CRT 调试时基础结构:没有 ASSERTs 等诊断信息,没有内存损坏检查,没有内存泄漏检查等。
  • 许多有用的函数不可用。一切都必须从头开始重新发明和重新实现。

因此,代价相当高。请注意!

Using the Code

由于我们不使用 CRT,因此需要指定程序自己的入口点。我们可以通过使用 /ENTRY 链接器开关来实现。但是,MSDN 在入口点函数的签名方面是错误的。根据 "/ENTRY (Entry-Point Symbol)" 文章

函数必须使用 __stdcall 调用约定进行定义。参数和返回值必须按照 Win32 API 中为 WinMain(对于 .exe 文件)或 DllEntryPoint(对于 DLL)所记录的那样定义。

这是不正确的。程序入口点的实际函数签名必须是:int __cdecl(*)(void)int __stdcall(*)(void)。调用约定并不重要,因为函数不接收任何参数,因此无需清理堆栈。

以下是 Name2Clip 的全部代码

// Name2Clip.c : Defines the entry point for the application.
//

#define WIN32_LEAN_AND_MEAN		// Exclude rarely-used stuff from Windows headers
#define STRICT
#define _WIN32_WINNT    0x0500      // Win2K

#include <Windows.h>
#include <ShellApi.h>
#include <WindowsX.h>

#include "resource.h"

/////////////////////////////////////////////////////////////////////////////
//
UINT CalcDataLength(
    LPCWSTR* ppArgv,
    int nArgc,
    UINT nFlags);

LPWSTR FormatCopyData(
    LPCWSTR* ppArgv,
    int nArgc,
    UINT nFlags,
    LPWSTR pDst);

UINT GetFlags(
    LPCWSTR* ppArgv,
    int nArgc);

UINT GetKeyboardFlags();

UINT GetCmdLineFlags(
    LPCWSTR* ppArgv,
    int nArgc);

LPCWSTR FindFilename(
    LPCWSTR pcwsz);

void ShowUsage(void);

/////////////////////////////////////////////////////////////////////////////
//
#define N2C_CODEFRIENDLY    0x01    // /C switch
#define N2C_SHORTPATHS      0x02    // /S switch
#define N2C_NAMESONLY       0x04    // /N switch

/////////////////////////////////////////////////////////////////////////////
//
int wN2ClipMain(void)
{
    int nArgc = 0;
    LPCWSTR* ppArgv = CommandLineToArgvW(GetCommandLineW(), &nArgc);

    if(!ppArgv) return -1;

    if(nArgc > 1)
    {
        UINT nFlags = GetFlags(ppArgv, nArgc);

        UINT nDataLen = CalcDataLength(ppArgv, nArgc, nFlags);

        LPWSTR pwszCopy = GlobalAllocPtr(GMEM_MOVEABLE | GMEM_ZEROINIT,
            nDataLen * sizeof(WCHAR));

        if(pwszCopy)
        {
            HWND hWndDesktop = GetDesktopWindow();

            FormatCopyData(ppArgv, nArgc, nFlags, pwszCopy);
            GlobalUnlockPtr(pwszCopy);

            if(OpenClipboard(hWndDesktop))
            {
                if(EmptyClipboard())
                    SetClipboardData(CF_UNICODETEXT, GlobalPtrHandle(pwszCopy));

                CloseClipboard();
            }
        }

        GlobalFreePtr(ppArgv);
    }
    else ShowUsage();

    return 0;
}

UINT CalcDataLength(
    LPCWSTR* ppArgv,
    int nArgc,
    UINT nFlags)
{
    UINT nLen = 0;
    int i;

    for(i = 1; i < nArgc; i++)
    {
        int nExtraLen = 0;
        int nPathLen = 0;
        LPCWSTR pPath = ppArgv[i];

        if(*pPath == L'/') continue;    // it's a switch

        if(nFlags & N2C_NAMESONLY)
        {
            pPath = FindFilename(pPath);
        }
        else if(nFlags & N2C_CODEFRIENDLY)
        {
            LPCWSTR pEos = pPath;

            while(*pEos)
            {
                if(*pEos == '\\') nExtraLen++;
                pEos++;
            }
        }

        if(nFlags & N2C_SHORTPATHS)
        {
            if(nFlags & N2C_NAMESONLY)
                nPathLen = 12;  // 8.3 name
            else
                nPathLen = GetShortPathNameW(pPath, NULL, 0);
        }
        else
        {
            nPathLen = lstrlenW(pPath);
        }

        nLen += nPathLen + nExtraLen + 2 /*CR+LF*/;
    }

    nLen = nLen + 1 /*NULL*/;

    return nLen;
}

LPWSTR FormatCopyData(
    LPCWSTR* ppArgv,
    int nArgc,
    UINT nFlags,
    LPWSTR pDst)
{
    LPWSTR pCopyData = pDst;
    int i;
    WCHAR wszPath[MAX_PATH + 1];

    for(i = 1; i < nArgc; i++)
    {
        LPCWSTR pSrc = ppArgv[i];

        if(*pSrc == L'/') continue; // it's a switch

        if(nFlags & N2C_SHORTPATHS)
        {
            DWORD dwLen = GetShortPathNameW(pSrc, wszPath, MAX_PATH);

            if(dwLen != ERROR_INVALID_PARAMETER)
                pSrc = wszPath;
            /* else */
            /* The volume doesn't support short filenames */
        }

        if(nFlags & N2C_NAMESONLY)
            pSrc = FindFilename(pSrc);

        while((*pDst = *pSrc) != 0)
        {
            if((nFlags & N2C_CODEFRIENDLY) && (*pSrc == L'\\'))
            {
                pDst++; *pDst = L'\\';
            }

            pDst++; pSrc++;
        }

        if(i < nArgc - 1)
        {
            *pDst++ = L'\r';
            *pDst++ = L'\n';
        }
    }

    return pCopyData;
}

UINT GetFlags(
    LPCWSTR* ppArgv,
    int nArgc)
{
    UINT nKbdFlags = GetKeyboardFlags();
    UINT nCmdFlags = GetCmdLineFlags(ppArgv, nArgc);

    return (nKbdFlags | nCmdFlags);
}

UINT GetKeyboardFlags()
{
    UINT nFlags = 0;

    if(GetAsyncKeyState(VK_CONTROL) & 0x8000)
        nFlags |= N2C_CODEFRIENDLY;

    if(GetAsyncKeyState(VK_SHIFT) & 0x8000)
        nFlags |= N2C_SHORTPATHS;

    return nFlags;
}

UINT GetCmdLineFlags(
    LPCWSTR* ppArgv,
    int nArgc)
{
    UINT nFlags = 0;
    int i, j;

    const struct Switches
    {
        LPCWSTR pcwszSwitch;
        UINT    nFlag;
    } switches[] = {
        { L"/C",  N2C_CODEFRIENDLY },
        { L"/S",  N2C_SHORTPATHS   },
        { L"/N",  N2C_NAMESONLY    }
    };

    for(i = 0; i < nArgc; ++i)
    {
        for(j = 0; j < ARRAYSIZE(switches); ++j)
        {
            if(lstrcmpiW(ppArgv[i], switches[j].pcwszSwitch) == 0)
            {
                nFlags |= switches[j].nFlag;
                break;
            }
        }
    }

    return nFlags;
}

LPCWSTR FindFilename(
    LPCWSTR pcwsz)
{
    LPCWSTR pcwszStart = pcwsz;

    /* find end of string */
    while(*pcwsz++);

    /* search towards front */
    while(--pcwsz != pcwszStart && *pcwsz != L'\\' && *pcwsz != L'/');

    /* char found ? */
    if (*pcwsz == L'\\' || *pcwsz == L'/')
        pcwsz++;

    return pcwsz;
}

void ShowUsage(void)
{
    MSGBOXPARAMS mbp = { 0 };
    mbp.cbSize       = sizeof(MSGBOXPARAMS);
    mbp.hwndOwner    = GetDesktopWindow();
    mbp.hInstance    = GetModuleHandle(NULL);
    mbp.lpszText     = MAKEINTRESOURCE(IDS_USAGE);
    mbp.lpszCaption  = MAKEINTRESOURCE(IDS_CAPTION);
    mbp.dwStyle      = MB_USERICON | MB_OK;
    mbp.lpszIcon     = MAKEINTRESOURCE(IDI_NAME2CLIP);
    mbp.dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT);

    MessageBoxIndirect(&mbp);
}

已知限制

根据 MSDN,CreateProcess 函数的命令行字符串最大长度为 32,768 个字符。Name2Clip 由 Windows 资源管理器启动,并将选定的文件名作为命令行参数。因此,使用 Name2Clip 发送到剪贴板的文件数量存在限制。即使 32K 个字符不是一个小缓冲区,有时也可能超出。当选定的文件过多时,系统会显示以下对话框

Command line is too long

解决方法是一次复制较少的文件名。

历史

  • 2009 年 8 月 29 日:初始发布
  • 2009 年 10 月 26 日:文章已更新
© . All rights reserved.