Compensating for a Serious Bug in RICHED20.DLL
|
|
I was having a problem with using highlighting in a Rich Edit control (using VS.NET 2003). I noticed that there was a certain degree of "color creep" in my output. What was interesting was that there was a pattern to it.
This particular application was generated as an MDI application using a view derived from CRichEditView:
I found that the "creep" was based on the actual line position. This can be illustrated in the following sample output, which naively assumes that if it selects the same characters it just wrote, it will change their characteristics. This naive assumption that the documentation actually doesn't lie to you results in rather unexpected output. What is amazing is that there is no KB article that documents this bug, which is apparently of long-standing.
The intended output would have been
A bit of effort revealed that the error is due to the fact that the GetTextLength call returns erroneous data. It returns the number of characters that would be required on a GetWindowText, which is not related to the number of characters in the control. The documentation of GetTextLength references the WM_GETTEXTLENGHT, which says "returns a value that is larger than the actual length of the text. This occurs with certain mixtures of ANSI and Unicode, and is due to the system allowing for the possible existence of double-byte character set (DBCS) characters within the text...to obtain the exact length of the text, use the WM_GETTEXT...message". In other words, there is no reliable way to determine the end of the buffer, because GetWindowText will return the translated CRLF! This means that you can never, ever know how many characters are actually in the buffer!
Of course, in reading this, I realized none of it applied to what I was doing, because I did not have a mix of ANSI and Unicode, was not using a DBCS, and in fact had a plain-vanilla ANSI app. The information that the window text returned is incorrect even in the simplest case is not documented anywhere.
So the trick, on doing an append operation, is to recompute the end-of-buffer based on
int start = c_text.GetTextLength(); c_text.SetSel(start, start); CHARRANGE highlight; c_text.GetSel(highlight);
This compensates for the complete screwup where you can't actually ask about where the end of the buffer is!
The function which appends text in this sample is shown below.
void CRichedBugDemoView::Append(const CString & s, CHARFORMAT2 & fmt) { CRichEditCtrl & c_text = GetRichEditCtrl(); // [1] CHARRANGE oldsel; c_text.GetSel(oldsel); // [2] int oldfirstline = c_text.GetTextLength(); // [3] c_text.SetRedraw(FALSE); // avoid annoying flicker // [4] int start = c_text.GetTextLength(); // [5] c_text.SetSel(start, start); // [6] CHARRANGE newsel; // [7] c_text.GetSel(newsel); // [8] CHARRANGE highlight = newsel; // [8] c_text.ReplaceSel(s); // [9] int end = c_text.GetTextLength(); // [10] CHARRANGE newend = {end, end}; // [11] c_text.SetSel(newend); // [12] c_text.GetSel(newend); // [13] highlight.cpMax = newsel.cpMax; // [14] c_text.SetSel(highlight.cpMin, highlight.cpMax); // [15] c_text.SetSelectionCharFormat(fmt); // [16] if(oldsel.cpMin != newsel.cpMin || oldsel.cpMax != newsel.cpMax) // [17] { /* restore selection */ c_text.SetSel(oldsel); // [18] int current = c_text.GetFirstVisibleLine(); // [29] if(current != oldfirstline) // [20] c_text.LineScroll(oldfirstline - current); // [21] } /* restore selection */ else c_text.SetSel(highlight.cpMax, highlight.cpMax); // [22] c_text.SetRedraw(TRUE); // [23] c_text.Invalidate(); // [24] } // CRichedBugDemoView::Append
The code which writes the sample is
void CRichedBugDemoView::WriteOneSample() { CHARFORMAT2 fmt; ::ZeroMemory(&fmt, sizeof(fmt)); // // expressed as HTML this would be // <i>This</i> is a <b>test</b> of <font color="red">color</font> and <u>underlining</u> // <i> fmt.dwMask = CFM_ITALIC; fmt.dwEffects = CFE_ITALIC; Append(_T("This"), fmt); // </i> fmt.dwMask = CFM_ITALIC; fmt.dwEffects &= ~CFE_ITALIC; Append(_T(" is a "), fmt); // <b> fmt.dwMask |= CFM_BOLD; fmt.dwEffects |= CFE_BOLD; Append(_T("test"), fmt); // </b> fmt.dwEffects &= ~CFE_BOLD; Append(_T(" of "), fmt); // <font color="red"> fmt.dwMask |= CFM_COLOR; fmt.crTextColor = RGB(255, 0, 0); Append(_T("color"), fmt); // </font> fmt.crTextColor = ::GetSysColor(COLOR_WINDOWTEXT); Append(_T(" and "), fmt); // <u> fmt.dwMask |= CFM_UNDERLINE; fmt.dwEffects = CFE_UNDERLINE; Append(_T("underlining"), fmt); // </u> fmt.dwEffects &= ~CFE_UNDERLINE; Append(_T("\r\n"), fmt); } // CRichedBugDemoView::WriteOneSample
Note that you must explicitly turn off styles; otherwise, they will persist with the additional text which is added. I could have turned them off by appending an empty string with the style turned off, but I just chose to do this as part of the writing of the next block of text.
The bottom line here is that the Rich Edit control does not work like CEdit; the GetTextLength (EM_GETTEXTLENGTH) message lies about the length of the text, and it lies in ways that are undocumented.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft. In fact, I am absolutely certain that most of this essay is not endorsed by Microsoft. But it should have been during the VS.NET design process!