Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Word add-in to syntax highlight selected text

0.00/5 (No votes)
2 Feb 2007 2  
A Word addin to syntax highlight selected text. The toolbar is permanent, with a transparent button icon.

Sample Image - Addin_Shot.gif

Introduction

The Syntax Highlight add-in is a COM add-in for Microsoft Word. Through a COM add-in, we can add a menu item or toolbar button onto Word, implementing special functions which we need.

The add-in on this page can change the color or background color of a selected text. How to do this is not fixed. Anyone can define a new rule for changing color, by implementing the functions which are declared in the interface header file. This add-in provides an open method to operate with text in Word. If you are interested in parsing text, this add-in can help you parse text too.

This software consists of a main module (sntxaddn.dll) and a set of highlight rules.

References

Basically, as an add-in, the principle of this software is the same as the one in: Building an Office2K COM add-in with VC++/ATL, which is an add-in for Outlook.

So, this article will mostly describe the special points of this add-in itself.

Interesting & Special Points

1. Permanent, not temporary CommandBar

A temporary command bar will be removed when Word is closed, and will be re-created when Word is opened. If you drag the newly created command bar to a position, the position will be lost after Word is re-opened. Instead, a permanent command bar will not be removed when Word is closed.

But a new problem is emitted: "When to remove the command bar if the user want to uninstalls it?" My solution: "Remove the command bar in DllUnregisterServer."

Create permanent, not temporary 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 old button if exists:
// 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;
    }
}
Remove command bar in '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. Transparent button icon

In order to be compatible with Office 2000, I used 'PasteFace' to transfer a button icon. According to Microsoft documentation, I was going to use "Toolbar Button Face" and "Toolbar Button Mask" as the clipboard format names.

But I met a problem, 'PasteFace' failed to transfer the transparent button icon if the Office software is not the English version. Indeed, the clipboard format name should be localized if Office software is the German or Chinese version etc.

I asked in many forums, but nobody was able to say anything about it. Afterwards, I found a way to determine the format names by myself.

// 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. Backup and restore clipboard data around using 'PasteFace'

The 'CopyFace' and 'PasteFace' operations will destroy any old clipboard data, which is copied into the clipboard before Word is opened, and the old clipboard data may be what you are going to paste into Word.

Then, backup the old clipboard data before the toolbar button's Create process:

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(); 

Restore the clipboard after the toolbar button's creation is completed:

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. Interface of the custom syntax rule

This add-in can load custom rules dynamically. The interface header file is:

// 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

How to implement 'Syntax Rule' easily

Each 'syntax rule' is a single dll file which implements those API's above.

I have provided a 'static library' (lib_syntax.lib) to help you to write 'syntax rule'. This 'static library' has implemented the key API's: 'Parse', 'GetNextPos', and 'GetColor'. Everything you need to do is to provide a 'regular expression pattern' and a 'color definition map'. For example:

// 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;
}

The main point about the 'regular expression pattern' is that: I used 'Named group' of 'regular expression'.

DEELX regular expression engine for C++ supports 'Named group', so I used DEELX in the lib_syntax.lib. DEELX regular expression engine is discussed at codeproject.com, and introduced at DEELX homepage: http://www.regexlab.com/deelx/ .

Please pay attention to the 'run-time library' of your dll project and the lib_syntax.lib library. They must be the same before they can be linked successfully.

References and Acknowledgements

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here