Msftedit.dll과 CF_RTF
영어나 우리말로 잘 되어있는 문서도 없고, 라틴어?? (맞나??)로 된 글을 복사해둡니다. 참고하시길...
Note sul controllo Rich Edit
[di Matteo Mecucci – v.1.3 del 28 settembre 2006]
Introduzione
Come per tante altre parti delle API Win32, anche il Rich Edit a volte si comporta in modi inattesi o
non documentati, perciò mi piacerebbe appuntare qui alcune note relative al suo uso. La lista
crescerà man mano che avrò tempo di stilarla e man mano che i problemi si presenteranno.
In questi giorni in ufficio stiamo creando un software che usa intensamente un controllo rich–text
full–featured. Per realizzarlo ho scelto di partire dal Rich Edit Control di base delle API Win32,
contenuto in un controllo custom che ne gestisca gli eventi e l'interazione con le altre parti del
software. Questa scelta ha portato ovviamente alcuni vantaggi e alcuni svantaggi ma per i nostri
scopi al momento sembra che possiamo accontentarci e possiamo raggiungere più o meno ogni
obbiettivo previsto.
Licenza
Questo documento è distribuito con Licenza Creative Commons BY-NC-SA 2.0. I
termini della licenza sono disponibili presso il sito:
http://creativecommons.org/licenses/by-nc-sa/2.0/it/
Eventuali marchi e copyright nominati ed utilizzati in questo documento appartengono ai rispettivi
proprietari.
Il materiale fornito è da considerarsi "AS IS" e l'autore declina ogni responsabilità per danni
derivanti dal suo utilizzo.
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 1
La versione del controllo da utilizzare
La scelta della versione del controllo da utilizzare non è necessariamente banale. Sebbene
siano "papabili" per il controllo sia la classe RICHEDIT_CLASS (versione 2.0 o 3.0) che la classe
MSFTEDIT_CLASS (versione 4.1 disponibile con XP SP1), quest'ultima ha un supporto
decisamente migliore delle tabelle RTF (non al livello di Word, comunque) e offre un'anteprima
di stampa più fedele, ma renderizza in modo pessimo le tabelle con colonne a larghezza
automatica (assegna a tutte larghezza nulla, mentre la versione precedente gli assegna una larghezza
fissa). Inoltre l'ultima versione del controllo non supporta ‘nativamente’ il set di caratteri ANSI ma
solo quello UTF–16.
Se si sceglie il controllo MSFTEDIT_CLASS, bisogna fare attenzione alle impostazioni del
progetto rispetto all'encoding dei caratteri. Se si utilizza, come accade nella maggior parte dei
progetti legacy, la versione ANSI e non quella Unicode UTF–16 delle API, ci sono alcuni punti su
cui soffermarsi.
Prima di tutto non può essere utilizzato nel CreateWindow il define della classe perché anche
quello è una stringa wide, quindi bisogna usare direttamente il nome della classe
"RichEdit50W" (notate che al contrario delle versioni precedenti non esiste alcuna classe
"RichEdit50A").
m_pRichEditDll = ::LoadLibrary("msftedit.dll");
if(m_pRichEditDll != NULL)
{
// si assicura che i controlli avanzati siano attivi
INITCOMMONCONTROLSEX icc;
icc.dwSize = sizeof(INITCOMMONCONTROLSEX);
icc.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&icc);
// crea il controllo rich text
m_pRTHwnd = ::CreateWindowEx(exStyle, TEXT("RICHEDIT50W"), TEXT(""),
style, 0, 0, rc.width(), rc.height(), m_pHwnd, NULL, NULL, NULL);
}
Si deve anche tenere presente che tutti i messaggi al controllo possono essere inviati
tranquillamente con le strutture in versione Ansi (ad esempio EM_SETCHARFORMAT può
utilizzare CHARFORMAT = CHARFORMATA) in quanto vengono inviati per default con
SendMessage = SendMessageA, TRANNE i messaggi che LEGGONO il testo dal controllo,
ovvero EM_GETSELTEXT e EM_GETTEXTRANGE, in quanto i buffer vengono comunque
interpretati come wchar_t* e quindi riempiti con testo Unicode, non Ansi; quindi va utilizzato nel
primo caso un buffer wchar_t* e nel secondo la struttura TEXTRANGEW e non TEXTRANGE.
Nessun problema invece per i messaggi che impostano il testo, come EM_REPLACESEL o
EM_SETTEXTEX.
// copia in textw tutto il testo del controllo in formato UTF-16
wchar_t* textw = (wchar_t*)malloc(...);
// dimensione opportuna
TEXTRANGEW tr = { {0, -1}, textw };
int read = ::SendMessage(m_pRTHwnd, EM_GETTEXTRANGE, 0, (LPARAM)&tr);
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 2
Impostazioni del formato del testo
Tutte le misure del rich text sono espresse in twip: 1 cm = 567 twips; 1 pollice = 1440 twip.
Questi numeri possono essere utili ad esempio nei CHARFORMAT e nei PARAFORMAT.
Per evitare il wordwrap del testo è necessario inviare il messaggio EM_SETTARGETDEVICE
con wParam=0 e lParam=1. Se si vuole il wordwrap (come di default), lParam va messo pari a 0.
Nella stampa questo messaggio va utilizzato in modo più accorto, impostando il device context
della stampante come wParam e la larghezza del foglio come lParam.
// disattiva il wordwrap in base ad un flag
::SendMessage(m_pRTHwnd, EM_SETTARGETDEVICE, NULL, m_fTextOnlyMode ? 1 : 0);
Per poter utilizzare l'allineamento giustificato è necessario prima aver impostato il flag
TO_ADVANCEDTYPOGRAPHY col messaggio EM_SETTYPOGRAPHYOPTIONS.
Per utilizzare il controllo RichEdit come editor di testo non formattato imposto il flag
SES_EMULATESYSEDIT
nel
messaggio
EM_SETEDITSTYLE,
il
flag
TO_SIMPLELINEBREAK nel messaggio EM_SETTYPOGRAPHYOPTIONS. La cosa più
seccante però è la necessità di filtrare il tentativo di incollare testo formattato nel controllo: ne
parliamo in maggiore dettaglio in seguito. Basti dire che il flag TM_PLAINTEXT nel messaggio
EM_SETTEXTMODE, che la documentazione indica come sufficiente allo scopo sembra non avere
alcun effetto sul problema.
// imposta i flag in base al flag text-only
::SendMessage(m_pRTHwnd, EM_SETTYPOGRAPHYOPTIONS,
m_fTextOnlyMode ? TO_SIMPLELINEBREAK : TO_ADVANCEDTYPOGRAPHY,
m_fTextOnlyMode ? TO_SIMPLELINEBREAK : TO_ADVANCEDTYPOGRAPHY);
::SendMessage(m_pRTHwnd, EM_SETEDITSTYLE,
m_fTextOnlyMode ? SES_EMULATESYSEDIT : 0, SES_EMULATESYSEDIT);
Il default per il massimo di caratteri che il controllo accetta è in molte occasioni basso: 2
15
(32767) caratteri. È sufficiente impostarlo con EM_EXLIMITTEXT, mettendo in lParam un
numero più alto (noi usiamo ad esempio 0xffffff = 2
24
-1). Questo messaggio deve essere inviato
quando il controllo è vuoto.
// svuota il controllo ed imposta il numero di caratteri che esso accetta
SETTEXTEX st = {ST_DEFAULT, CP_ACP};
::SendMessage(m_pRTHwnd,EM_SETTEXTEX,(WPARAM)&st,(LPARAM)TEXT(""));
::SendMessage(m_pRTHwnd, EM_EXLIMITTEXT, 0, 0xffffff);
Quando si aggiunge del testo non RTF al controllo esso assume la formattazione corrente ma
alla fine viene reimpostata quella di default. Se tenete alla formattazione esistente nel punto di
inserimento, abbiate cura di chiedere la formattazione con EM_GETCHARFORMAT prima di
inserire il testo e di reimpostarla con EM_SETCHARFORMAT dopo averlo inserito. Fate anche
attenzione che aggiungere del testo programmaticamente non sposta il cursore nel controllo: se
dovete aggiungere testo di seguito ogni volta dovete anche spostare il cursore.
// inserisce del testo UTF-16 non formattato in fondo al controllo
::SendMessage(m_pRTHwnd, EM_SETSEL, (WPARAM)-1, (LPARAM)-1);
CHARFORMAT2 cf;
cf.cbSize = sizeof(cf);
::SendMessage(m_pRTHwnd, EM_GETCHARFORMAT, SCF_SELECTION, ( LPARAM ) &cf );
cf.dwMask = CFM_ALL2;
SETTEXTEX st = {ST_SELECTION, 1200};
::SendMessage(m_pRTHwnd, EM_SETTEXTEX, (WPARAM)&st, (LPARAM)textw);
::SendMessage(m_pRTHwnd, EM_SETCHARFORMAT, SCF_SELECTION, ( LPARAM ) &cf );
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 3
Creare una tabella direttamente attraverso PARAFORMAT non è possibile (i pochi attributi
relativi sono in sola lettura e non possono essere impostati in un EM_SETPARAFORMAT). Il
metodo migliore che abbiamo trovato è stato quello di costruire un testo RTF che descrivesse la
tabella voluta e inserirlo nel controllo già bello e pronto. Documentazione essenziale per lo scopo
la specifica RTF della Microsoft, ovviamente.
La numerazione con PFM_NUMBERING sa contare solo fino a 255. Se si imposta il numero
iniziale ad X con PFM_NUMBERINGSTART, il massimo numero che si può ottenere (senza
reimpostare a mano PFM_NUMBERINGSTART) è X+255.
Per quanto strano, il numero o il pallino della numerazione assumono il CHARFORMAT
dell'accapo della riga dove si trovano: se si vuole il numero in grassetto, ad esempio, è necessario
impostare il CFM_BOLD dell'ultimo carattere della riga dove si trova il numero.
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 4
Sovraccaricare la gestione degli eventi di default
Si possono facilmente intercettare eventi di mouse, tastiera e altro genere col messaggio
EM_SETEVENTMASK e gestendo i messaggi sul controllo parent.
Si può ad esempio bloccare un tasto per impedirgli di fare edit e collegarlo ad una funzione sul
testo — è sufficiente restituire un valore non nullo nella WindowProc in risposta all'evento
EN_MSGFILTER relativo; ho usato questo metodo ad esempio per gestire il Tab e lo Shift-Tab
per aumentare e diminuire l'indentazione di una selezione, mentre senza selezione il Tab
aggiunge una tabulazione, come di default. Inoltre gestisco l'evento EN_SELCHANGE per
aggiornare la toolbar rispetto al CHARFORMAT e al PARAFORMAT del testo selezionato e
l'evento EN_CHANGE per impostare un documento come modificato.
// frammento della WindowProc del controllo parent del rich-text
if(uMsg == WM_NOTIFY &&((LPNMHDR)lParam)->code == EN_MSGFILTER)
{
// controlla se il tasto premuto e' tab e se c'e' una selezione attiva.
// in questo caso fa l'indentazione del testo selezionato
// e non la sostituzione col carattere di tabulazione.
MSGFILTER* filter = (MSGFILTER*)lParam;
if(filter->msg==WM_CHAR &&filter->wParam=='\t')
{
CHARRANGE cr;
SendMessage(((LPNMHDR)lParam)->hwndFrom, EM_EXGETSEL, 0, (LPARAM)&cr);
if(cr.cpMax != cr.cpMin)
{
if(IsShiftDown())
ctrl->Outdent();
else
ctrl->Indent();
return 1;
}
}
}
Per verificare se il controllo è stato modificato è sufficiente nella maggior parte dei casi usare
il messaggio EM_GETMODIFY. Se però prevedete di poter inserire oggetti OLE all'interno del
testo, dovete occuparvi voi di controllare se qualcuno di essi è modificato. Ho scelto di farlo
enumerando tutti gli oggetti attraverso l'interfaccia OLE del controllo ottenibile col messaggio
EM_GETOLEINTERFACE. Nell'enumerazione (ottenuta con GetObjectCount()/GetObject()),
controllo se l'oggetto corrente ha l'interfaccia IPersistStorage (con QueryInterface) e quindi
controllo le modifiche col metodo IsDirty().
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 5
// se il flag di modificato non e' settato, controlla
// prima che il controllo stesso non lo abbia attivo
m_fModified = ::SendMessage(m_pRTHwnd, EM_GETMODIFY, 0, 0)!=0;
if(m_fModified || !m_pOleObj)
return;
// poi controlla se un oggetto contenuto nel controllo e' per caso modificato
// andando a cercare il flag IsDirty sulla sua interfaccia IPersistStorage
HRESULT hr = 0;
int objectCount = m_pOleObj->GetObjectCount();
for (int i = 0; !m_fModified &&i <objectCount; i++)
{
REOBJECT reObj;
ZeroMemory(&reObj, sizeof(REOBJECT));
reObj.cbStruct = sizeof(REOBJECT);
hr = m_pOleObj->GetObject(i, &reObj, REO_GETOBJ_POLEOBJ);
if(SUCCEEDED(hr))
{
IPersistStorage* pstg=NULL;
if(SUCCEEDED(reObj.poleobj->QueryInterface(
IID_PPV_ARG(IPersistStorage, &pstg))))
{
if(pstg &&pstg->IsDirty()==S_OK)
m_fModified=true;
pstg->Release();
}
reObj.poleobj->Release();
}
}
Per intercettare alcuni eventi OLE è possibile impostare un oggetto di callback attraverso il
messaggio EM_SETOLECALLBACK. La classe dell'oggetto deve derivare dall'interfaccia
IRichEditOleCallback e può risultare utile (necessaria) per diverse cose, di cui parleremo in seguito.
La definizione della classe è agevolata dal fatto che tutti i metodi che non vogliamo implementare
possono semplicemente restituire E_NOTIMPL o in alcuni casi S_OK (ad esempio in
QueryInsertObject e in DeleteObject) per ottenere il comportamento di default. Fornendo l'oggetto
di callback è possibile:
•
inserire oggetti OLE nel controllo fornendo l'implementazione di GetNewStorage,
deputata a creare oggetti di storage.
// implementazione molto banale e poco efficiente di GetNewStorage
HRESULT TRichTextOleCallback::GetNewStorage(LPSTORAGE* ppStg)
{
if (!ppStg)
return E_INVALIDARG;
*ppStg = NULL;
LPLOCKBYTES pLockBytes;
HRESULT hr = CreateILockBytesOnHGlobal(NULL, TRUE, &pLockBytes);
if (FAILED(hr))
return hr;
hr = StgCreateDocfileOnILockBytes(pLockBytes, STGM_SHARE_EXCLUSIVE |
STGM_CREATE | STGM_READWRITE, 0, ppStg);
pLockBytes->Release();
return (hr);
}
•
gestire l'interazione con la nostra applicazione dei controlli che supportano
l'attivazione in–place, implementando i metodi GetInPlaceContext e ShowContainerUI.
Questa interazione coinvolge a fondo altre parti dell'applicazione (toolbar, menu, palette e
quant'altro?), quindi può essere di difficile attuazione in applicazioni legacy non realizzate
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 6
dall'inizio con certi criteri. Tra l'altro non è sempre desiderabile che questa attivazione
inplace abbia luogo: in alcuni casi è molto meglio che con un doppio click si apra
l'applicazione relativa all'oggetto OLE nel suo ambiente esclusivo, senza interagire
direttamente col nostro; è molto comodo il fatto che questo sia esattamente il
comportamento di default.
•
filtrare gli oggetti OLE che si possono inserire (magari con un paste dalla clipboard, con
un caricamento da file o con un dialogo "Inserisci oggetto?") implementando il metodo
QueryInsertObject.
•
filtrare dati in ingresso dalla clipboard o dal drag&drop (come vedremo più avanti)
implementando il metodo QueryAcceptData.
•
fornire un help basato sul contesto implementando il metodo ContextSensitiveHelp.
•
mostrare un menu contestuale in risposta al tasto destro del mouse (praticamente
obbligatorio), implementando il metodo GetContextMenu.
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 7
Salvataggio e caricamento
Serializzare il contenuto del controllo su file RTF o su un buffer in memoria è molto semplice
attraverso il messaggio EM_STREAMOUT. Questo messaggio permette anche di specificare
un'eventuale pagina di codici per la codifica del testo. In particolare mi sembrava molto interessante
la possibilità di usare la codifica UTF–8, usando come wParam «(CP_UTF8 <<16) |
SF_USECODEPAGE | SF_RTF», in base alla documentazione. Ebbene queste impostazioni
generano file perfettamente validi (come si verifica leggendo il file attraverso un relativo
EM_STREAMIN) ma purtroppo non leggibili da Word e da Wordpad. A parte la questione assurda
che il Wordpad nient'altro dovrebbe essere che un contenitore per lo stesso controllo che stiamo
realizzando (usa l'mfstedit), il motivo si legge nella specifica dell'RTF (della Microsoft,
ovviamente): il tag \urtf1, quello utilizzato in questa codifica, è generato dall'applicazione "Pocket
Word" (della Microsoft, ovviamente) e NON viene riconosciuto da Word. Non riesco a spiegarmi
questa cosa. Comunque per i file bisogna di fatto usare SF_RTF e basta. Il problema è relativo
per fortuna, in quanto la specifica RTF prevede comunque precise modalità per la memorizzazione
di caratteri Unicode, quindi non si perde niente…
Anche salvare un pezzo di testo selezionato e non tutto il contenuto come file RTF è
abbastanza semplice. È sufficiente utilizzare il metodo GetClipboardData dell'oggetto
IRichEditOle ottenibile con il messaggio EM_GETOLEINTERFACE. Questo metodo restituisce un
oggetto IDataObject per un certo range di caratteri, che è possibile interrogare con un opportuno
FORMATETC per ottenere i dati relativi nel formato desiderato (CF_RTF o CF_TEXT nel nostro
caso).
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 8
Bug e work–around
Come accennato prima, quando si utilizza il controllo richtext in modalità solo testo si ha lo
spiacevole comportamento di default che per quanti flag si impostino il testo formattato continua
a poter entrare nel controllo dalla clipboard o con i drag&drop. Per evitare tutto ciò è
necessario, come accennato, implementare la funzione QueryAcceptData dell'interfaccia
IRichTextOleCallback. Al suo interno va controllato innanzitutto se ci viene richiesto il formato che
preferiamo (quando *pcfFormat==0): in tal caso specifichiamo che vogliamo ovviamente
CF_TEXT. Se al contrario il formato è imposto, si restituisce DATA_E_FORMATETC (per
convenzione, ma va bene un errore qualsiasi) se esso è appunto diverso da CF_TEXT.
Con questo metodo è uscito fuori però un bacherello (almeno pare tale): il richtext elimina
l'eventuale end–of–line dell'ultima riga del testo da incollare: se ad esempio si copia una linea
intera di testo, compreso l'accapo, quando eseguiamo l'incolla l'accapo non viene incollato. Questo
comportamento è ancora più evidente quando si fa il drag&drop del testo: se una linea intera di
testo viene spostata e poi il drop viene annullato o finisce nella posizione originale, l'accapo viene
eliminato. Per evitare tutto ciò possiamo modificare ancora la QueryAcceptData in modo che,
quando il paste stia avvenendo (segnalato da fReally==1), essa esegua direttamente l'operazione,
chiedendo al pDataObj i dati CF_TEXT col metodo GetData ed inserendoli nel controllo con un
EM_SETTEXTEX. A quel punto essa può restituire S_FALSE, ovvero un codice di successo ma
negativo che indica al chiamante che il dato è stato accettato ma che si è già provveduto
all'inserimento. Infatti restituire S_OK farebbe inserire un'altra volta il testo mentre restituire un
codice d'errore eviterebbe ad esempio la cancellazione del testo originale in un drag&drop.
// implementazione del metodo QueryAcceptData per gestire correttamente
// la copia del testo non formattato
HRESULT TrichTextOleCallback::QueryAcceptData(
LPDATAOBJECT pDataObj, CLIPFORMAT* pcfFormat,
DWORD reco, BOOL fReally, HGLOBAL hMetaPict)
{
// se il controllo e' in modalita' solo testo, pretende di ottenere
// il formato CF_TEXT, altrimenti restituisce errore
if(m_pCtrl &&m_pCtrl->IsInTextOnlyMode())
{
// workaround per inserire gli accapo alla fine del testo
if(fReally)
{
FORMATETC ftc; STGMEDIUM stg;
ftc.cfFormat = CF_TEXT;
ftc.dwAspect = DVASPECT_CONTENT;
ftc.lindex = -1; ftc.ptd = NULL;
ftc.tymed = TYMED_HGLOBAL;
if(pDataObj &&SUCCEEDED(pDataObj->GetData(&ftc, &stg)) &&
stg.hGlobal)
{
char* text = (char*)GlobalLock(stg.hGlobal);
if(text)
{
// inserisce il testo al posto della selezione corrente
m_pCtrl->InsertText(text);
GlobalUnlock(stg.hGlobal);
}
if(stg.pUnkForRelease)
stg.pUnkForRelease->Release();
else
GlobalFree(stg.hGlobal);
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 9
if(text)
// codice per dire che questa funzione ha accettato
// il testo ma l'ha anche inserito direttamente lei
return S_FALSE;
}
}
if(*pcfFormat == 0)
{
// chiediamo solo testo non formattato
*pcfFormat = CF_TEXT;
return S_OK;
}
if(*pcfFormat != CF_TEXT) // errore: formato non valido
return DATA_E_FORMATETC;
}
return S_OK;
}
In alcuni strani casi in cui nella clipboard c'è tra i formati disponibili "RTF in UTF8", ci è capitato
di ottenere un testo vuoto utilizzando il WM_PASTE standard. Per questo abbiamo preferito
intercettare i comandi di incolla (non col QueryAcceptData ma direttamente da dove viene inviato il
WM_PASTE) e chiedere esplicitamente l'inserimento dei dati CF_RTF con un bel
EM_PASTESPECIAL.
// workaround necessario per strani casi in cui c'e' "RTF in UTF8"
// tra i formati nella clipboard
static int cf_rtf = RegisterClipboardFormat(CF_RTF);
if(!m_fTextOnlyMode &&::SendMessage(m_pRTHwnd, EM_CANPASTE, cf_rtf, 0))
{
::SendMessage(m_pRTHwnd, EM_PASTESPECIAL, cf_rtf, 0);
return;
}
::SendMessage(m_pRTHwnd, WM_PASTE, 0, 0);
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 10
Riferimenti
Descrizione del RichEdit Control su MSDN:
http://msdn.microsoft.com/library/en-us/shellcc/platform/commctls/RichEdit/RichEditControls.asp
Rich Text Format Specification, version 1.8:
http://go.microsoft.com/?linkid=5467681
Il mio sito:
© 2006 Matteo Mecucci, DigitalWaters.net
pag. 11