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

一个 Word 插件,用于对选中文本进行语法高亮

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (12投票s)

2006年5月18日

CPOL

3分钟阅读

viewsIcon

120473

downloadIcon

3175

一个用于语法高亮显示所选文本的 Word 插件。工具栏是永久性的,带有一个透明的按钮图标。

Sample Image - Addin_Shot.gif

引言

语法高亮插件是 Microsoft Word 的一个 COM 插件。通过 COM 插件,我们可以向 Word 添加菜单项或工具栏按钮,实现我们需要的特殊功能。

此页面上的插件可以更改所选文本的颜色或背景颜色。如何做到这一点不是固定的。任何人都可以通过实现接口头文件中声明的函数来定义更改颜色的新规则。此插件提供了一种操作 Word 中文本的开放方法。如果您对文本解析感兴趣,此插件也可以帮助您解析文本。

此软件由主模块 (sntxaddn.dll) 和一组高亮规则组成。

参考文献

基本上,作为一个插件,此软件的原理与以下文章中介绍的 Outlook 插件相同:使用 VC++/ATL 构建 Office2K COM 插件

因此,本文主要描述此插件本身的特殊之处。

有趣且特殊的点

1. 永久性,而非临时 CommandBar

临时命令栏会在 Word 关闭时被删除,并在 Word 打开时重新创建。如果将新创建的命令栏拖动到某个位置,则在 Word 重新打开后该位置将会丢失。相反,永久性命令栏在 Word 关闭时不会被删除。

但出现了一个新问题:“如果用户想要卸载它,何时删除命令栏?”我的解决方案:“在DllUnregisterServer中删除命令栏。”

创建永久性,而非临时 CommandBar
CComPtr <_CommandBars> spCmdBars = wordApp->
    GetCommandBars();
CComPtr <CommandBar> spNewCmdBar = NULL;
 
// attach command bar if already exists
HRESULT hr = spCmdBars->get_Item(_variant_t(RSTR(
    IDS_SYNTAX_BAR)), &spNewCmdBar);
 
if( FAILED(hr) || spNewCmdBar == NULL )
{
    // New Create if not exists
    CComVariant vName (RSTR(IDS_SYNTAX_BAR));
    CComVariant vPos  (msoBarFloating);
    CComVariant vTemp (VARIANT_FALSE); //menu is NOT temporary 
    CComVariant vEmpty(DISP_E_PARAMNOTFOUND, VT_ERROR);
    spNewCmdBar = spCmdBars->Add(vName, vPos, vEmpty, vTemp);
}
如果存在则附加旧按钮
// attach buttons
int btncnt = spBarControls->GetCount();
for(i=btncnt; i>=1; i--)
{
    CComPtr <CommandBarControl> spNewBar = 
       spBarControls->GetItem(_variant_t((long)i));
    CComQIPtr <_CommandBarButton> spButton( spNewBar );
    _bstr_t tag = spButton->GetTag();
    CSyntaxModule * pSyntax = NULL;
 
    for(int j=0; j<MAX_BUTTONS; j++)
    {
        if( m_pSyntax[j] == NULL )
            break;
 
        if( m_pSyntax[j]->m_szSyntaxName == tag )
        {
            pSyntax = m_pSyntax[j];
            break;
        }
    }
 
    if( pSyntax != NULL )
    {
        // keep reference to button disp
        m_spButtons[nAttachIndex ++] = spButton;
        DispEventAdvise(nAttachIndex, (IDispatch*)spButton);
        pSyntax->m_bAttached = TRUE;
    }
    else
    {
        spButton->Delete();
        bUpdated = TRUE;
    }
}
在 'DllUnregisterServer' 中删除命令栏
//
// Attach to Running Word instance
// or Create New Word Application instance
//
CComPtr <_Application> CWordSntxAddnApp::AttachRunningWord()
{
    HRESULT hr;
    CLSID   clsid;
    hr = ::CLSIDFromProgID(L"Word.Application", &clsid);
 
    if(FAILED(hr))
    {
        return NULL;
    }
 
    IUnknown  * pUnknown = NULL;
    hr = ::GetActiveObject(clsid, NULL, &pUnknown);
    if( !FAILED(hr) )
    {
        return CComQIPtr <_Application> ( pUnknown );
    }
    else
    {
        COleDispatchDriver ddrv;
        ddrv.CreateDispatch(clsid);
        return CComQIPtr <_Application> (ddrv.m_lpDispatch);
    }
}
VOID CWordSntxAddn::RemoveCommandBar(CComPtr <_Application> & wordApp)
{
    CComPtr <_CommandBars> spCmdBars = wordApp->GetCommandBars();
    CComPtr <CommandBar> spSyntaxCmdBar = NULL;
 
    HRESULT hr = 
            spCmdBars->get_Item(_variant_t(RSTR(IDS_SYNTAX_BAR)), 
            &spSyntaxCmdBar);
    if( !FAILED(hr) && spSyntaxCmdBar != NULL )
    {
        spSyntaxCmdBar->Delete();
 
        // Save
        CComQIPtr <Template> custom( wordApp->GetCustomizationContext() );
        custom->Save();
    }
}

2. 透明按钮图标

为了与 Office 2000 兼容,我使用了 'PasteFace' 来传输按钮图标。根据Microsoft 文档,我打算使用“工具栏按钮面”和“工具栏按钮蒙版”作为剪贴板格式名称。

但是我遇到了一个问题,如果 Office 软件不是英文版本,'PasteFace' 无法传输透明按钮图标。实际上,如果 Office 软件是德文版或中文版等,则剪贴板格式名称应该被本地化。

我在许多论坛上提问,但没有人能够对此发表任何意见。之后,我找到了一种自己确定格式名称的方法。

// to determine localized 'format name'
::OpenClipboard(NULL);
::EmptyClipboard();
::SetClipboardData(CF_BITMAP, 
     ::LoadBitmap(_Module.GetResourceInstance(), 
     MAKEINTRESOURCE(IDB_HIGHLIGHT)));
::CloseClipboard();
 
spButton->PasteFace();
spButton->CopyFace();
 
// Init
_tcscpy(m_szFaceFormatName, _T("Toolbar Button Face"));
_tcscpy(m_szMaskFormatName, _T("Toolbar Button Mask"));
 
// loop
::OpenClipboard(NULL);
 
UINT  format = 0, n = 0, fmtsize[2] = {0};
TCHAR fmtnames[2][100] = {0};
 
while( (format = ::EnumClipboardFormats(format)) != 0 )
{
    if( format > 17 && GetClipboardFormatName(format, 
                              fmtnames[n], 100) != 0 )
    {
        fmtsize[n++] = ::GlobalSize(::GetClipboardData(format));
        if( n >= 2 ) break;
    }
}
 
if( fmtsize[0] > fmtsize[1] )
{
    if( fmtnames[0][0] )
       _tcscpy(m_szFaceFormatName, fmtnames[0]);
    if( fmtnames[1][0] )
       _tcscpy(m_szMaskFormatName, fmtnames[1]);
}
else
{
    if( fmtnames[1][0] )
        _tcscpy(m_szFaceFormatName, fmtnames[1]);
    if( fmtnames[0][0] )
        _tcscpy(m_szMaskFormatName, fmtnames[0]);
}
 
::CloseClipboard();

3. 使用 'PasteFace' 备份和恢复剪贴板数据

'CopyFace' 和 'PasteFace' 操作会破坏任何旧的剪贴板数据,这些数据是在 Word 打开之前复制到剪贴板中的,而旧的剪贴板数据可能是您将要粘贴到 Word 中的内容。

然后,在工具栏按钮的创建过程之前备份旧的剪贴板数据

if( ! ::OpenClipboard(NULL) )
    return FALSE;
 
UINT format = 0;
while( (format = ::EnumClipboardFormats(format)) != 0 )
{
    ClipboardData data;
    data.m_nFormat = format;

    // skip some formats
    if( format == CF_BITMAP || format == CF_METAFILEPICT ||
        format == CF_PALETTE || format == CF_OWNERDISPLAY ||
        format == CF_DSPMETAFILEPICT || format == CF_DSPBITMAP ||
        ( format >= CF_PRIVATEFIRST && format <= CF_PRIVATELAST ) )
    {
        continue;
    }

    // get format name
    if( format <= 14 )
        data.m_szFormatName[0] = 0;
    else if( GetClipboardFormatName(format, 
                  data.m_szFormatName, 100) == 0 )
        data.m_szFormatName[0] = 0;

    // get handle
    HANDLE hMem = ::GetClipboardData( format );
    if( hMem == NULL )
        continue;
 
    // copy handle
    switch( format )
    {
    case CF_ENHMETAFILE:
    case CF_DSPENHMETAFILE:
        data.m_hData = 
            ::CopyMetaFile((HMETAFILE)hMem, NULL);
        break;
 
    default:
        {
            int    size = ::GlobalSize(hMem);
            LPVOID pMem = ::GlobalLock( hMem );
 
            data.m_hData   = ::GlobalAlloc( GMEM_MOVEABLE | 
                                            GMEM_DDESHARE, size );
            LPVOID pNewMem = ::GlobalLock( data.m_hData );
 
            memcpy(pNewMem, pMem, size);
 
            ::GlobalUnlock(hMem);
            ::GlobalUnlock(data.m_hData);
        }
    }
 
    m_lstData.AddTail(data);
}
::CloseClipboard(); 

工具栏按钮创建完成后恢复剪贴板

if( ! ::OpenClipboard(NULL) )
    return FALSE;
 
::EmptyClipboard();
 
POSITION pos = m_lstData.GetHeadPosition();
while( pos != NULL )
{
    ClipboardData & data = m_lstData.GetNext( pos );
 
    UINT format = data.m_nFormat;
 
    if( data.m_szFormatName[0] != 0 )
    {
        UINT u = RegisterClipboardFormat( data.m_szFormatName );
        if( u > 0 ) format = u;
    }
 
    ::SetClipboardData( format, data.m_hData );
}
 
::CloseClipboard(); 

4. 自定义语法规则的接口

此插件可以动态加载自定义规则。接口头文件是

// API
#define SYNTAX_MOD_API __declspec(dllexport)
 
// extern "C"
#ifdef __cplusplus
    extern "C" {
#endif
 
// Caption
LPCSTR SYNTAX_MOD_API GetCaption();
 
// Tooltip ( Optional )
LPCSTR SYNTAX_MOD_API GetTooltip();
 
// Button Face ( Optional )
HBITMAP SYNTAX_MOD_API GetBtnFace();
 
// Parse
VOID SYNTAX_MOD_API Parse(LPWSTR text, INT len);
 
// Nextpos
BOOL SYNTAX_MOD_API GetNextPos(INT & pos, INT & len);
 
// Color
BOOL SYNTAX_MOD_API GetColor(INT & color);
 
// Background Color ( Optional )
BOOL SYNTAX_MOD_API GetBgColor(INT & bgcolor);

#ifdef __cplusplus
    }
#endif

如何轻松实现“语法规则”

每个“语法规则”都是一个单独的 dll 文件,它实现了上述 API。

我提供了一个“静态库”(lib_syntax.lib)来帮助您编写“语法规则”。这个“静态库”实现了关键 API:'Parse'、'GetNextPos' 和 'GetColor'。您需要做的就是提供一个“正则表达式模式”和一个“颜色定义映射”。例如

// regular expression pattern
LPCWSTR pattern = L"(?<upper>[A-Z]+)|(?<lower>[a-z]+)|(?<number>[0-9]+)";

// color
ColorMap map[] =
{
    { L"upper" , RGB(0xff, 0, 0) },
    { L"lower" , RGB(0, 0xff, 0) },
    { L"number", RGB(0, 0, 0xff) },
};

// entrance
BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD ul_reason_for_call, 
                       LPVOID lpReserved
)
{
    if( ul_reason_for_call == DLL_PROCESS_ATTACH )
    {
        // when initialization
        InitSyntax(
            pattern,
            0,
            map,
            sizeof(map)/sizeof(ColorMap)
        );
    }

    return TRUE;
}

关于“正则表达式模式”的主要点是:我使用了“正则表达式”的“命名组”。

DEELX C++ 正则表达式引擎支持“命名组”,因此我在 lib_syntax.lib 中使用了 DEELX。DEELX 正则表达式引擎在codeproject.com上进行了讨论,并在 DEELX 主页上进行了介绍:http://www.regexlab.com/deelx/

请注意您的 dll 项目和 lib_syntax.lib 库的“运行时库”。它们必须相同才能成功链接。

参考和致谢

© . All rights reserved.