
Introduction
I'm a little nervous to introduce my first code here. CHiliteEditView
is a small reusable class derived from CEditView
that highlights the current caret line, something like the famous editor program UltraEdit does. I wrote this class just for fun, and I like MFC very much.
How to use?
Using CHiliteEditView
in your project is fairly easy, just add HiliteEditView.cpp and HiliteEditView.h to your project, and add the following include line:
#include "HiliteEditView.h"
to your stdafx.h, then replace CEditView
with CHiliteEditView
in yourview.cpp and yourview.h files. Then compile and run.
If your project doesn't use CEditView
as the base class, but another class, and you want to try this funny stuff, all you have to do is change the view's base class to CHiliteEditView
, in the class declaration and implementation and message map declarations.
Details
I think it is easier for me to make it than to explain how I made it. First problem is when to highlight the caret line? The answer is: after the control's normal paint action. When we enter some text, or click the mouse, or scroll the view, the Edit control will repaint itself. If we want to paint some extra stuff, it is a great chance to do so at that time, just after the control's normal repaint actions.
Obviously, we must handle the following messages:
WM_PAINT
- main place when the control needs repainting.
WM_LBUTTONDOWN
- when we click in the control's area, the caret may have moved to another line, so we have to highlight the new line and repaint the previous highlighted line to its normal state.
WM_KEYDOWN
- when we move the caret by arrow keys, or PgDn/PgUp etc...
WM_MOUSEMOVE
- when we drag to select, it also needs to update the hilite bar.
WM_CHAR
- why is it needed? Isn't WM_KEYDOWN
enough? Actually I first wrote the class in CRichEditView
without handling WM_CHAR
and it worked just fine. But when I tried to rewrite it in CEditView
, it didn't repaint when I entered a char. Finally I found the reason: when entering a char in RichEdit control, it updates the screen by sending a WM_PAINT
; this doesn't happen with the Edit control - it updates the screen by drawing on the screen directly in the WM_CHAR
message handler (or others?) and does not send a WM_PAINT
message.
Methods
int GetCaretLine() const
- returns the line number which contains the caret.
void GetLineRect(int nLine, LPRECT lpRect) const
- gets the rectangle bound of the specified line in client coordinates.
virtual void DrawCaretLine(BOOL bInPaint = FALSE)
- highlights the line containing the caret. Can be overridden to implement other kinds of hilite.
I would say some more words on DrawCaretLine()
.
void CHiliteEditView::DrawCaretLine(BOOL bInPaint)
{
int nLine = GetCaretLine();
if (nLine == m_nCaretLine && !bInPaint)
return;
CRect rectClip;
GetEditCtrl().GetRect(rectClip);
CClientDC dc(this);
dc.IntersectClipRect(rectClip);
int nLineFirst = GetEditCtrl().GetFirstVisibleLine();
int nLineLast = GetEditCtrl().LineFromChar(
GetEditCtrl().CharFromPos(
rectClip.BottomRight()
)
);
HideCaret();
if (m_nCaretLine >= nLineFirst && m_nCaretLine <= nLineLast)
{
m_bCanPaint = FALSE;
CRect rect;
GetLineRect(m_nCaretLine, rect);
InvalidateRect(rect, FALSE);
UpdateWindow();
m_bCanPaint = TRUE;
}
if (nLine >= nLineFirst && nLine <= nLineLast)
{
CRect rect;
GetLineRect(nLine, rect);
CDC dcMem;
dcMem.CreateCompatibleDC(&dc);
CBitmap bmp;
bmp.CreateCompatibleBitmap(&dc, rect.Width(), rect.Height());
CBitmap* pSaveBmp = dcMem.SelectObject(&bmp);
CBrush br(RGB(0, 0, 255));
dcMem.FillRect(CRect(0, 0, rect.Width(), rect.Height()), &br);
dcMem.BitBlt(0, 0, rect.Width(), rect.Height(),
&dc, rect.left, rect.top, SRCINVERT
);
dc.BitBlt(rect.left, rect.top, rect.Width(), rect.Height(),
&dcMem, 0, 0, SRCCOPY
);
dcMem.SelectObject(pSaveBmp);
}
ShowCaret();
m_nCaretLine = nLine;
}
First, we compare the previous and current caret lines to decide if we need to paint. Normally we needn't repaint twice in the same line, but when handling WM_PAINT
, it always needs to repaint because of scrolling. This is the purpose of the BOOL
parameter bInPaint
. In the WM_PAINT
handler, we call DrawCaretLine()
with bInPaint
set to TRUE
, and in other cases, we just forget the parameter, which is default to FALSE
.
When the caret position changes, before we paint a hilite bar in the new place, we have to erase the old one. We simply invalidate the previous line rectangle and force WM_PAINT
to handle it. There is another important thing here to indicate that the WM_PAINT
handler must not call DrawCaretLine()
in this case, otherwise it will go into a loop. The OnPaint()
handler looks like this:
void CHiliteEditView::OnPaint()
{
Default();
if (m_bCanPaint)
DrawCaretLine(TRUE);
}
So we can set m_bCanPaint
to TRUE
to disable this loop, and restore m_bCanPaint
to FALSE
immediately after repainting.
Finally, I appreciate your enduring my angularity English. Thank you!