cwinapiundorichedittom.h

Text highlighting and UNDO in Richedit


I'm trying to implement a text highlighting and undo in RichEdit. The code below marks 5 and 6 chars but undo doesn't work (the text doesn't change) though Edit_CanUndo returns 1. If I turn off a updateHighlighting then undo works but it rolls back too much at once (e.g. input a long text, paste a text by Ctrl+V, input another text - it's required only 3 undo to rollback all).

What is wrong with my code? Maybe I should return a specific value from updateHighlighting?

#define UNICODE
#define _UNICODE

#include <tchar.h>
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <richedit.h>

#define IDC_EDITOR 1000
#define IDC_BUTTON 1001

HWND hEditorWnd;

bool updateHighlighting(HWND hWnd) {
    int carriagePos = 0;
    SendMessage(hWnd, EM_GETSEL, (WPARAM)&carriagePos, (WPARAM)&carriagePos);

    CHARFORMAT cf = {0};
    cf.cbSize = sizeof(CHARFORMAT) ;
    cf.dwMask = CFM_COLOR | CFM_BOLD;
    cf.crTextColor = RGB(0, 0, 200);
    cf.dwEffects = CFM_BOLD;
    SendMessage(hWnd, EM_SETSEL, 0, -1);
    SendMessage(hWnd, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM) &cf);

    cf.dwMask = CFM_COLOR | CFM_BOLD;
    cf.dwEffects = cf.dwEffects & ~CFM_BOLD;
    cf.crTextColor = RGB(200, 0, 0);
    SendMessage(hWnd, EM_SETSEL, 4, 6);
    SendMessage(hWnd, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM) &cf);
    SendMessage(hWnd, EM_SETSEL, carriagePos, carriagePos);

    _tprintf(TEXT("End highlight\n"));
    return true;
}

LRESULT CALLBACK WindowProcedure (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
        case WM_DESTROY:
            PostQuitMessage (0);
            break;

        case WM_COMMAND: {
            if (LOWORD(wParam) == IDC_EDITOR && HIWORD(wParam) == EN_CHANGE)
                return updateHighlighting(hEditorWnd);

            if (LOWORD(wParam) == IDC_BUTTON && HIWORD(wParam) == BN_CLICKED)
                _tprintf(TEXT("UNDO: can %i => result: %i\n"), Edit_CanUndo(hEditorWnd), Edit_Undo(hEditorWnd));
        }
        break;

        default:
            return DefWindowProc (hWnd, msg, wParam, lParam);
    }

    return 0;
}

int WINAPI WinMain (HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nCmdShow) {
    InitCommonControls();
    LoadLibrary(TEXT("msftedit.dll"));

    HWND hWnd;
    MSG msg;
    WNDCLASSEX wincl;

    wincl.hInstance = hThisInstance;
    wincl.lpszClassName = TEXT("AppClass");
    wincl.lpfnWndProc = WindowProcedure;
    wincl.style = CS_DBLCLKS;
    wincl.cbSize = sizeof (WNDCLASSEX);
    wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
    wincl.lpszMenuName = NULL;
    wincl.cbClsExtra = 0;
    wincl.cbWndExtra = 0;
    wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;

    if (!RegisterClassEx (&wincl))
        return 0;

    hWnd = CreateWindowEx (0, TEXT("AppClass"), TEXT("Test"), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 300, 400, 310, 375, HWND_DESKTOP, NULL, hThisInstance, NULL);

    CreateWindowEx(0, WC_BUTTON , L"UNDO", WS_VISIBLE | WS_CHILD | WS_TABSTOP, 10, 310, 280, 30, hWnd, (HMENU)IDC_BUTTON, hThisInstance,  NULL);
    hEditorWnd = CreateWindowEx(0, TEXT("RICHEDIT50W"), NULL, WS_VISIBLE | WS_CHILD | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_WANTRETURN | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP | ES_NOHIDESEL, 0, 0, 300, 300, hWnd, (HMENU)IDC_EDITOR, hThisInstance,  NULL);
    SendMessage(hEditorWnd, EM_SETEVENTMASK, 0, ENM_CHANGE | ENM_UPDATE | ENM_KEYEVENTS);

    while (GetMessage (&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

P.S. I use Code::Block 17.12 + gcc5.1 on Win7x64 (but build a 32bit app).


Solution

  • What you mean exactly with "doesn't work" or with when it "does work" is unclear, however, changing color is something that can be undone too so if you do not want that, then you have to turn off undo before coloring and resume it after coloring.

    I don't know if there are easier ways to do it, but in C I use the following functions:

    //#include <windows, richedit, ...>
    #include <TOM.h>
    
    extern HWND ghEditWnd;
    
    static IUnknown *pUnk;
    static ITextDocument *pDoc;
    
    static const IID IID_ITextDocument = {
        0x8CC497C0, 0xA1DF, 0x11CE,
        {0x80,0x98,0x00,0xAA,0x00,0x47,0xBE,0x5D}
    };
    
    static int tomInit(void)
    {
        if (!pUnk) SendMessage(ghEditWnd, EM_GETOLEINTERFACE, 0, (LPARAM)&pUnk);
        if (!pUnk || pUnk->lpVtbl->QueryInterface(pUnk,&IID_ITextDocument, &pDoc) != NOERROR)
            {pUnk= 0; return FALSE;}
        return TRUE;
    }
    void undoSuspend(void)
    {
        if (!pUnk) if (!tomInit()) return;
        pDoc->lpVtbl->Undo(pDoc,tomSuspend,0); //Suspends Undo.
    }
    void undoResume(void)
    {
        if (!pUnk) if (!tomInit()) return;
        pDoc->lpVtbl->Undo(pDoc,tomResume,0);   // resume Undo.
    }
    void releaseTOM(void)
    {
        if (pUnk) {
            pUnk->lpVtbl->Release(pUnk); pUnk= 0; pDoc= 0;
        }
    }
    

    See also https://learn.microsoft.com/en-us/windows/win32/api/tom/

    EDIT: in question How to disable Undo in Rich Text Box in WPF? the suggestion was to set UndoLimit to zero. It is not clear if that would clear the existing undo stack too.

    Undo can be suspended and resumed in RichEdit 3.0, Microsoft says.