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

代码片段管理器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (24投票s)

2011年7月3日

CPOL

10分钟阅读

viewsIcon

84667

downloadIcon

1008

一个可以方便地将代码或文本片段放入剪贴板的小工具。

目录

引言

本文展示了如何创建一个小型应用程序,它位于任务栏上,并提供将预格式化文本或代码块复制到剪贴板的功能,然后可以根据需要将其粘贴到程序、文档或网页中。

Snippets.png

背景

我最初创建这个应用程序是因为我发现我反复将相同的评论或建议粘贴到 CodeProject 问答和论坛中。我还将其用作学习(我希望也能教学)示例,用于展示在开发 Win32 程序时可以在许多地方使用的一些功能。对于那些喜欢使用 MFC 的人来说,移植这些想法应该是一个相当简单的过程;这是留给读者的一个练习。

Using the Code

注意:本项目是在 Windows 7 和 Visual C++ Express 2010 下开发的。有关适用于 Windows XP 的版本,请参阅标题为 Windows XP 版本 的部分。该程序被分解为不同的区域,试图展示每个部分如何可以独立于整个项目使用。一些想法取自其他 CodeProject 文章*、MSDN,当然还有精彩的 Google。

*遗憾的是,我没有记录我查阅过的所有文章,因此向这些作者表示歉意,但如果您在此处看到任何可能源自您的文章的内容,请告诉我,我将添加致谢。

用于构建项目的程序文件是

  • 头文件
    • Snippets.h:本地项目和资源定义
    • StdAfx.h:整个项目中使用的包含头文件
  • 资源文件
    • Snippets.ico:通知区域图标
    • Snippets.rc:资源脚本 - 菜单、对话框、字符串、版本等。
  • 源文件
    • DataReader.cpp:用于从各种源读取 string 并保存到向量中的代码
    • NotifyIcon.cpp:用于设置通知图标和响应命令的代码
    • Snippets.cpp:Windows 应用程序的主要部分和消息处理程序
    • StdAfx.cpp:用于生成预编译头文件

创建任务栏通知应用程序

此应用程序的基础是任务栏通知图标,它允许通过放置在 Windows 桌面任务栏通知区域中的小图标来访问应用程序。应用程序本身是一个“正常”的 Windows 应用程序,其主窗口通常默认不显示(在某些情况下甚至从不显示)。但是,应用程序需要创建一个主窗口才能接收来自 Windows 操作系统的消息,以执行用户请求的任何操作。

该应用程序是一个简单的 Win32 Windows 应用程序,遵循以下标准模式:

  • 定义并注册一个 Windows 类及其 WndProc 消息处理程序
  • 创建此类的主要窗口并(可能)在屏幕上显示它 - 但在我们的例子中不显示
  • 启动消息泵以将用户消息分派给应用程序
  • 在接收到消息和命令时处理它们

通知图标所需的额外代码在启动消息循环之前调用,包括下面的部分,该部分将我们的图标添加到任务栏的通知区域。

// Use a globally unique id to identify our application icon (created by guidgen.exe)
static const GUID SnippetIconGUID =
{
    0xe87633eb, 0xaf17, 0x47e4, 0xa4, 0x3c, 0xaf, 0xbd, 0x86, 0xe1, 0x76, 0xd5
};

// the parameter block to register our notification icon
static NOTIFYICONDATA	notifyIconData;

/// <summary>
/// Initialize the taskbar icon for notification messages
/// </summary>
///
/// <param name="hWnd">handle to the parent window</param>
/// <param name="hInstance">instance handle to this module</param>
///
/// <returns>TRUE if successful initialisation</returns>
///
BOOL NotifyInit(HWND       hWnd,
                HINSTANCE  hInstance
                )
{
    // get source data into our vector
    pvContents = ReadData(MAKEINTRESOURCE(IDS_REGKEY), EX_REGVALUE);  // from the registry
    //  pvContents = ReadData(_T("..\\snippets.xml"), EX_XMLFILE);    // or an XML file
    if (pvContents != NULL &&
        pvContents->size() > 0)
    {
        SetLastError(-1);
        return FALSE;    // data read failure
    }
    
    // create the notification icon and
    // add it to the notification area of the Windows desktop.
    notifyIconData.cbSize           = sizeof notifyIconData;   	// size of the struct 
							// in bytes
    notifyIconData.hWnd             = hWnd;                    	// our main window
    notifyIconData.uID              = IDR_MAINFRAME;           	// the icon is identified 
							// by GUID (see guidItem) 
							// in Vista/7
    notifyIconData.uFlags           = NIF_MESSAGE |            	// uCallbackMessage member 
							// is valid
                                      NIF_ICON |               	// hIcon member is valid
                                      NIF_TIP |                	// Tooltip (szTip) member 
							// is valid
                                      NIF_INFO |               	// Balloon (szInfo) 
							// member is valid
                                      NIF_GUID |               	// GUID member is valid
                                      NIF_SHOWTIP;             	// use standard tooltip 
							// behaviour (see uVersion)
    notifyIconData.uCallbackMessage = WM_USER_SHELLICON;       	// notification message 
							// for user activations
    notifyIconData.hIcon            = LoadIcon(hInstance, 
		MAKEINTRESOURCE(IDR_MAINFRAME));           // 32x32 icon to be displayed
    LoadString(hInstance, IDS_TOOLTIP, notifyIconData.szTip, 
		_countof(notifyIconData.szTip));        // tip shown on mouse over
    notifyIconData.dwState          = 0;                       	// not used
    notifyIconData.dwStateMask      = 0;                       	// not used
    LoadString(hInstance, IDS_BALLOONINFO, notifyIconData.szInfo, 
	_countof(notifyIconData.szInfo));  		// balloon text shown on activation
    notifyIconData.uVersion         = NOTIFYICON_VERSION_4;   	// Vista and later
    LoadString(hInstance, IDS_BALLOONTITLE, notifyIconData.szInfoTitle, 
		_countof(notifyIconData.szInfoTitle));  	// balloon title
    notifyIconData.dwInfoFlags      = NIIF_USER;           	// use the hIcon icon 
							// in the balloon
#if (_WIN32_WINNT >= 0x0600)        // if Windows Vista or later
    notifyIconData.guidItem         = SnippetIconGUID;       	// icon GUID, used by 
							// windows for message 
							// tracking
    notifyIconData.hBalloonIcon     = NULL;
#endif

    return Shell_NotifyIcon(NIM_ADD, &notifyIconData);
} 

上述注意事项

  • 存储在 uCallbackMessage 字段中的值由 Windows 用于通知应用程序图标已被用户(通过鼠标或键盘)激活。此值应大于 WM_USER 的值,并且在应用程序中是唯一的。
  • uVersion 字段应根据 Vista 之前的 Windows 版本进行调整。
  • guidItem 的 GUID 应由 guidgen 或类似工具生成。
  • 工具提示和气球信息字段的大小按 NOTIFYICONDATA 结构定义。
  • Shell_NotifyIcon() 函数将图标添加到通知区域并显示气球信息几秒钟。
  • ReadData() 函数返回一个指向向量的指针,该向量包含多个 pair 对象,每个对象包含一对 string:一个标题和一个值,用于创建如下所述的菜单。

用于代码片段标识符及其关联内容的实际数据存储在一个列表中,该列表可以从多个不同来源创建。最常见的方法是在应用程序的某个源模块或资源脚本中包含一个 string 表,但这意味着每次需要更改列表时都需要重新构建应用程序。以下部分描述了两种可能的替代方案。

存储应用程序配置数据的常见方法之一是将其放入注册表中的一个为相关应用程序创建的键下。尽管这种方法现在经常受到批评,但作为学习如何利用此 Windows 功能的练习,它仍然值得讨论。

为了此程序的需要,我首先创建了一个包含我想要的信息的注册表数据文件,该文件将保存在以下键下

    HKEY_CURRENT_USER\
        Software\
            CodeProject\
                Snippets  

实际数据将作为值存储,其名称是将在应用程序的上下文菜单上显示的 string,其值将在选择菜单项时复制到剪贴板。这些数据可以手动加载到注册表中,但更简单的方法是创建一个 .reg 文件,该文件可以使用记事本或任何其他文本编辑器轻松编辑。然后通过双击 .reg 文件或使用 reg IMPORTregedit /s 命令将这些键添加到注册表。

.reg 文件的内容看起来像这样,其中每个条目包含两个带引号的 string,第一个成为项目的名称,第二个成为其值

    Windows Registry Editor Version 5.00

    [HKEY_CURRENT_USER\Software\CodeProject\Snippets]
    "Use <pre> tags"="Use the <span style=\"background-color:#FBEDBB;\">
	code block</span> button, or add &lt;pre&gt;&lt;/pre&gt; tags"
    "Petzold"="<a href=\"http://www.charlespetzold.com/dotnet/\">.NET Book Zero</a>
	[<a href=\"http://www.charlespetzold.com/dotnet/\" target=\"_blank\" 
	title=\"New Window\">^</a>]"
    "My Settings"="Select \"My Settings\" in the drop down below your name 
		at the top right"

这些值通过 RegEnumValue() 函数(参见 DataReader.cpp)从注册表提取到我们的向量中,如下所示

    // repeat this until we run out of elements
    for (dwIndex = 0; lStatus == ERROR_SUCCESS; ++dwIndex)
    {
        // set the buffer size for the value name
        cchValueName = _countof(szKeyText);
        // set the buffer size for the value content to zero
        cbData = 0;
        // get the value name and size of the content
        lStatus = RegEnumValue(hkSnippets,      // the registry key obtained above
                               dwIndex,         // index to the next value [ 0 .. n ]
                               szKeyText,       // value name returned here
                               &cchValueName,   // size of value name buffer in characters
                               NULL,            // reserved
                               NULL,            // value type returned here if necessary
                               NULL,            // value content not returned this time
                               &cbData);        // size of value data in bytes 
                                                // returned here
        if (lStatus == ERROR_SUCCESS)
        {
            // allocate the data buffer and get the content
            pData = new BYTE[cbData];
#if (_WIN32_WINNT >= 0x0600)		// if Windows Vista or later
            lStatus = RegGetValue(hkSnippets, NULL, szKeyText, 
			RRF_RT_REG_SZ, NULL, pData, &cbData);
#else
            lStatus = RegQueryValueEx(hkSnippets, szKeyText, NULL, NULL, pData, &cbData);
#endif
        }
        if (lStatus == ERROR_SUCCESS)
        {
            // create the vector if required
            if (pvPairs == NULL)
            {
                pvPairs = new VECPAIR();
            }
            // create a pair of strings and push down into the vector
            PTSTR pszContent = reinterpret_cast<PTSTR>(pData);
			      STRPAIR itemPair(szKeyText, pszContent);
			      pvPairs->push_back(itemPair);
			      // delete the value buffer for next time round
			      delete[] pData;
        }
    }

该函数通过如上所示的简单循环,提取键中每个值条目的名称,并可选地提取其内容。当每个条目被提取时,数据用于创建一个新的 pair 对象,然后将其推入用于保存所有项目的向量中。当所有值都已枚举时,RegEnumValue() 函数返回 ERROR_NO_MORE_ITEMS 状态。

另一种,也可能是更好的方法,是从 XML 文件读取数据。由于 Microsoft 提供了 XMLLite 库,这并不像可能的那样困难:请参阅 本文程序员指南

XML 数据的结构可以是开发人员希望的任何形式,然后编写提取代码以处理元素和属性节点,以流式方式向前读取 XML。为了实现我常用的代码片段,当值项包含特殊字符(包括 <>)时,我使用了 CDATA 节点,否则这些字符是不允许的。因此,我创建的 XML 数据包含以下形式的项目,与前面注册表值部分中显示的项目相对应

    <?xml version="1.0"?>
    <snippets xmlns:dt="urn:schemas-microsoft-com:datatypes">
      <snippet title="Use pre tags">
        <![CDATA[
        Please use the <span style="background-color:#FBEDBB;">code block
		</span> button, or add &lt;pre&gt;&lt;/pre&gt; tags
        ]]>
      </snippet>
      <snippet title="Petzold">
        <![CDATA[
        <a href="http://www.charlespetzold.com/dotnet/">.NET Book Zero</a>
	[<a href="http://www.charlespetzold.com/dotnet/" target="_blank" 
	title="New Window">^</a>]
        ]]>
      </snippet>
      <snippet title="My Settings">
        Select "My Settings" in the drop down below your name at the top right
      </snippet>
    </snippets>  

如您所见,这不包含架构信息,因此我们需要确保其内容格式正确,以避免元素丢失的问题。

用于读取和处理 XML 文件的实际代码也在 DataReader.cpp 源文件中,下面包含一个简短的摘录。与注册表值一样,数据通过重复读取 XML 流的节点并保存属性 string 作为键名,以及 CDATAtext 元素作为其相应值来提取。

使用 XMLLite 库访问源时的第一个要求是将源文件连接到 XmlReader 类,这通过使用 IStream 接口完成。最简单的方法是使用 ShellAPI 库的 SHCreateStreamOnFile() 函数,如下所示。注意:如果您希望将 XML 数据存储在简单的文本文件之外的某个位置,那么在本地实现 IStream 接口并不太困难。

读取器的启动代码创建了一个流和一个读取器,在读取器上设置了 DtdProcessing_Prohibit 属性以防止它查找附加的 DTD,并将流连接到读取器,准备提取流中的各个元素。

    // Open a read-only input stream and connect it to the XMLReader
    if (FAILED(hr = SHCreateStreamOnFile(pszSource, STGM_READ, &pFileStream)) ||
        FAILED(hr = CreateXmlReader(__uuidof(IXmlReader), 
			reinterpret_cast<PVOID*>(&pReader), NULL)) ||
        FAILED(hr = pReader->SetProperty(XmlReaderProperty_DtdProcessing, 
			DtdProcessing_Prohibit)) ||
        FAILED(hr = pReader->SetInput(pFileStream)))
    {
        return NULL;
    }

读取 XML 的实际代码是一个简单的循环,它逐节点遍历 XML 并根据需要处理内容。以以上数据为例,我们检查每个元素是否是 snippet 标签,如果是,则保存 title 属性,然后继续到下一个节点,该节点将添加到 pair 对象,从而添加到我们的向量中,如下所示。为了简洁起见,这里省略了代码块的一些部分;完整的例程请参见 DataReader.cpp 模块。整个例程逐节点遍历文件并按如下方式处理:

  • 如果它是一个开始元素,获取 title 属性并保存到第一个 string
  • 如果它是一个结束元素并且两个 string 都存在,则将该对保存到向量中
  • 如果它是文本或 CDATA,则将文本(修剪后)存储到第二个 string
// read until there are no more nodes
while (S_OK == (hr = pReader->Read(&nodeType)))
{
    switch (nodeType)
    {
    case XmlNodeType_Element:
        // test for a start snippet element
        if (SUCCEEDED(hr = pReader->GetLocalName(&pszElementlName, NULL)) &&
            _tcscmp(pszElementlName, _T("snippet")) == 0)
        {
            // check for a 'title' attribute
            if (SUCCEEDED(hr = pReader->MoveToFirstAttribute()) &&
                SUCCEEDED(hr = pReader->GetLocalName(&pszAttributelName, NULL)) &&
                _tcscmp(pszAttributelName, _T("title")) == 0)
            {
                // add the title to the pair object
                if (SUCCEEDED(hr = pReader->GetValue(&pszValue, NULL)))
                {
                    strPair.first = pszValue;
                }
            }
        }
        break;
        
    case XmlNodeType_EndElement:
        if (SUCCEEDED(hr = pReader->GetLocalName(&pszElementlName, NULL)) &&
            _tcscmp(pszElementlName, _T("snippet")) == 0)
        {
            // end snippet element, so save the pair of strings
            if (strPair.first.size() > 0 && strPair.second.size() > 0)
            {
                if (pvPairs == NULL)
                    pvPairs = new VECPAIR();
                pvPairs->push_back(strPair);
            }
            // clear for next time
            strPair.first.clear();
            strPair.second.clear();
        }
        break;
        
    case XmlNodeType_Text:
    case XmlNodeType_CDATA:
        if (SUCCEEDED(hr = pReader->GetValue(&pszValue, NULL)))
        {
            // text or CDATA goes to the second string of the pair
            if (!strPair.first.empty())
            {
                strPair.second = pszValue;
            }
        }
        break;
    }
}  

构建动态上下文菜单

本节讨论用户在通知区域选择图标并右键单击以调出上下文菜单时所采取的操作。消息通过我们的 WM_USER_SHELLICON 消息发送到主窗口。此消息由 NotifyIcon.cpp 模块中的 SnipNotify() 函数处理,它仅检查右键单击并交给 OnRButtonDown() 函数。此函数使用包含 string 对的向量中的标题值创建弹出菜单。每个菜单项都添加到弹出菜单的开头,并根据其在菜单中的位置给定一个命令 ID。然后跟踪菜单,让用户有机会选择特定项目、终止程序或只是切换到另一个窗口。创建菜单的代码遵循与以下类似的动态菜单的正常规则

// create a new menu that will contain the item titles
hSnipMenu = CreateMenu();
int idm = IDM_SNIP;
for (VECPAIR::iterator it = pvContents->begin(); it < pvContents->end(); ++it, ++idm)
{
    // add the titles and command ids to the new menu
    AppendMenu(hSnipMenu, MF_STRING, idm, it->first.c_str());
}  

创建此菜单后,我们现在需要将其添加到资源文件中的预定义菜单资源中,该资源如下所示

IDR_POPUPMENU MENU
BEGIN
    POPUP "PopUp"
    BEGIN
        // Snippet menu items will be added above here dynamically
        MENUITEM SEPARATOR
        MENUITEM	"E&xit",		ID_APP_EXIT
    END
END  

添加和显示菜单的代码是

// get a handle to the popup menu resource
hPopup = LoadMenu(GetModuleHandle(NULL), MAKEINTRESOURCE(IDR_POPUPMENU));
// and then a handle to the menu itself
hSubMenu = GetSubMenu(hPopup, 0);
// insert the snippets menu into the popup, before menuitem[0].
InsertMenu(hSubMenu, 0, MF_BYPOSITION | MF_POPUP | MF_STRING, 
			(UINT_PTR)hSnipMenu, _T("Snippets"));

// this call is required so that the menu will be dismissed if the user clicks into
// a different window rather than selecting a menu item.
SetForegroundWindow(hWnd);
// track the popup and await a command
TrackPopupMenuEx(hSubMenu,                           // handle to our menu
    TPM_RIGHTALIGN | TPM_TOPALIGN | TPM_NONOTIFY,    // menu attributes
    pMousePoint->x,                                  // x and
    pMousePoint->y,                                  // y of mouse position
    hWnd,                                            // our main window handle
    NULL                                             // not used here
);  

请注意,此时程序的主窗口必须设置为前台窗口(即使它不可见),以便在用户单击弹出菜单以外的任何地方时可以关闭菜单。

如果用户选择菜单项,该项的命令 ID 将发送到主窗口消息处理程序,并传递给 NotifyIcon.cpp 中的 OnSnipSelect(),如下一节所述。

当选择菜单项时,OnSnipSelect() 例程接收一个命令 ID(加上一个基本值),该 ID 提供了用于创建弹出菜单的 string 向量的索引。使用此索引,程序从向量中相关的 pair 对象中提取内容值并将其复制到剪贴板。然后可以将此文本粘贴到任何窗口中,以满足用户/开发人员的任何目的。

向剪贴板添加文本是一个相当简单的过程,遵循如下所示的一般代码路径

  • 打开剪贴板并清除所有以前的内容
  • 分配一个全局内存缓冲区来保存文本
  • 将文本复制到全局缓冲区
  • 将指向缓冲区的指针和内容类型传递给剪贴板处理程序
  • 释放全局缓冲区并关闭剪贴板
    PTSTR        pszContent;    // this points to the string to be put on the clipboard
    if (OpenClipboard(hWnd))
    {
        // clear any previous content
        EmptyClipboard();
        
        // get the buffer size required and allocate it on the global heap
        nSize = _tcslen(pszContent) + 1;
        hGlobal = GlobalAlloc(GMEM_MOVEABLE, nSize * sizeof(TCHAR));
        if (hGlobal != NULL)
        {
            // Lock the handle and copy the text to the buffer.
            PTSTR pstrCopy = reinterpret_cast<PTSTR>(GlobalLock(hGlobal));
            _tcscpy_s(pstrCopy, nSize, pszContent);
            GlobalUnlock(hGlobal);
            
            // Place the data on the clipboard.
#if defined(UNICODE)
            SetClipboardData(CF_UNICODETEXT, hGlobal);
#else
            SetClipboardData(CF_TEXT, hGlobal);
#endif
            GlobalFree(hGlobal);
        }
        // Close the clipboard.
        CloseClipboard();
    }

Windows XP 版本

尽管原始项目是在 Windows 7 下构建的,但我已经添加了一个解决方案和项目文件 SnippetsXP.slnSnippetsXP.vcproj,用于在 Windows XP 下构建。代码更改最少,仅影响 DataReader.cpp 中注册表读取器的代码,以及 NotifyIcon.cpp 中通知设置的代码。

  • Registry 值的简单使用以及如何枚举它们
  • XMLLite
  • 动态菜单
  • 剪贴板

关注点

  • 使用剪贴板
  • 使用 XMLLite 库从 XML 文件中提取数据
  • 从注册表读取标识符和值

历史

  • 2011 年 7 月首次发布

© . All rights reserved.