Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

TRichEdit : Line numbers in margin

1,688 views
Skip to first unread message

Guillaume Giroux

unread,
Mar 15, 2000, 3:00:00 AM3/15/00
to
Hi,

I would like to have line numbers in the left margin of my TRichEdit, on the
side or on the control, like many code editors offer, but without being part
of the Text / Lines properties. All lines have the same height. Any
idea/pointer would be much appreciated.

Thanks,
Guillaume


Damon Chandler

unread,
Mar 15, 2000, 3:00:00 AM3/15/00
to
Hi Guillaume,

> I would like to have line numbers in the left margin of my TRichEdit, on the
> side or on the control, like many code editors offer, but without being part
> of the Text / Lines properties. All lines have the same height.

Ok, the first thing that you need to do is add a permanent margin to the
RichEdit control. Specifically, you need to adjust the "working area" a.k.a.
formatting rectangle of the RichEdit. This can be accomplished via the
EM_GETRECT and EM_SETRECT messages. First, use the EM_GETRECT message to
retrieve the current formatting rectangle, then offset it, and finally use the
EM_SETRECT message, specifying this offset rectangle. So for example...

RECT R;
SNDMSG(RichEdit1->Handle, EM_GETRECT, 0, reinterpret_cast<LPARAM>(&R));
R.left = R.left + 35;
SNDMSG(RichEdit1->Handle, EM_SETRECT, 0, reinterpret_cast<LPARAM>(&R));


This much is straightforward, however, there are some caveats. First, the
RichEdit must be visible for this to work. Next, the TRichEdit class recreates
the underlying window according to several property specifications, effectively
undoing the previous formatting rectangle changes. To work around the first
problem, simply set the formatting rectangle after the control has painted
itself. The second hurdle can be overcome by create a TRichEdit descendant and
augmenting the CreateWnd() member function (in which you set the formatting
rectangle).

To actually render the line numbers, you need to handle the WM_PAINT message
directly. Within the message handler, use the GetDC() API function to grab a
handle to the RichEdit's display device context, then perform any rendering to
this DC. Usually, iterative rendering approaches lead to flicker, so your best
bet is to use a double-buffering technique. That is, render all the line
numbers to a bitmap, then finally render the bitmap to the RichEdit's device
context in one shot. Also, to speed things up, you aonly want to render the
line numbers of the lines that are visible. You can determine the indices of
the first and last visible lines via a combination of the EM_CHARFROMPOS and
EM_EXLINEFROMCHAR messages. Here's an example...


// in header...
class TMyRichEdit : public TRichEdit
{
private:
Graphics::TBitmap *m_FMemBitmap;
RECT m_FRGutter;
bool m_FFirstPaint;

MESSAGE void __fastcall WMPaint(TMessage &Msg);

protected:
virtual void __fastcall CreateWnd();
virtual void __fastcall DrawGutter(TCanvas *ACanvas);
virtual void __fastcall DrawLineNumbers(TCanvas *ACanvas);

public:
__fastcall TMyRichEdit(TComponent *AOwner);
__fastcall ~TMyRichEdit();

BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_PAINT, TMessage, WMPaint)
END_MESSAGE_MAP(TRichEdit)
};


// in source...
__fastcall TMyRichEdit::TMyRichEdit(TComponent *AOwner)
: TRichEdit(AOwner), m_FFirstPaint(false)
{
// create a back-buffer bitmap
m_FMemBitmap = new Graphics::TBitmap();
m_FMemBitmap->Canvas->Brush->Color = clBtnFace;
}

__fastcall TMyRichEdit::~TMyRichEdit()
{
// destroy the back-buffer bitmap
delete m_FMemBitmap;
}

void __fastcall TMyRichEdit::CreateWnd()
{
TRichEdit::CreateWnd();

// implicitly reset the working rectangle
m_FFirstPaint = true;
}

void __fastcall TMyRichEdit::WMPaint(TMessage &Msg)
{
TRichEdit::Dispatch(&Msg);
if (m_FFirstPaint)
{
// once-only flag
m_FFirstPaint = false;

// set the working area
RECT R;
SNDMSG(Handle, EM_GETRECT, 0, reinterpret_cast<LPARAM>(&R));
R.left = R.left + 35; // set the width of the gutter
SNDMSG(Handle, EM_SETRECT, 0, reinterpret_cast<LPARAM>(&R));

// calculate the gutter rectangle
m_FRGutter = static_cast<RECT>(ClientRect);
m_FRGutter.right = R.left - 3;

// specify the back-buffer bitmap dimensions
m_FMemBitmap->Width = m_FRGutter.right - m_FRGutter.left;
m_FMemBitmap->Height = m_FRGutter.bottom - m_FRGutter.top;
}

// grab a handle to the device context
HDC Hdc = GetDC(Handle);
try
{
// render the gutter to the bitmap
DrawGutter(m_FMemBitmap->Canvas);
// render the line numbers to the bitmap
DrawLineNumbers(m_FMemBitmap->Canvas);

// block transfer the bitmap's bits to the display
// device context of the RichEdit control
BitBlt(Hdc, 0, 0,
m_FMemBitmap->Width, m_FMemBitmap->Height,
m_FMemBitmap->Canvas->Handle, 0, 0,
SRCCOPY);
}
catch (...)
{
// release the device context
ReleaseDC(Handle, Hdc);
}
// release the device context
ReleaseDC(Handle, Hdc);
}

void __fastcall TMyRichEdit::DrawGutter(TCanvas *ACanvas)
{
// render the gutter
ACanvas->Brush->Color = clBtnFace;
ACanvas->FillRect(ACanvas->ClipRect);
DrawEdge(ACanvas->Handle, &m_FRGutter, EDGE_BUMP, BF_RIGHT);
}

void __fastcall TMyRichEdit::DrawLineNumbers(TCanvas *ACanvas)
{
// change the font and ROP3 mode
ACanvas->Font = Font;
ACanvas->Brush->Style = bsClear;

// calculate the postion to render to
RECT RNumber = m_FRGutter;
InflateRect(&RNumber, -5, 0);
const int letter_height = ACanvas->TextHeight("A");
OffsetRect(&RNumber, 0, -letter_height + 2);

// find the index of the top visible line
POINTL POrg = {0, 0};
long char_index =
SNDMSG(Handle, EM_CHARFROMPOS, 0, reinterpret_cast<LPARAM>(&POrg));
long start_line = SNDMSG(Handle, EM_EXLINEFROMCHAR, 0, char_index);

// find the index of the last visible line
POINTL PEnd = {0, ClientHeight};
char_index =
SNDMSG(Handle, EM_CHARFROMPOS, 0, reinterpret_cast<LPARAM>(&PEnd));
long stop_line = SNDMSG(Handle, EM_EXLINEFROMCHAR, 0, char_index);

// render the line numbers
for (int index = start_line; index <= stop_line; ++index)
{
RNumber.top = RNumber.top + letter_height;
RNumber.bottom = RNumber.top + letter_height;

AnsiString line_number(index);
DrawText(ACanvas->Handle, line_number.c_str(), line_number.Length(),
&RNumber, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
}
}


HTH.

--
Damon Chandler
http://bcbcaq.freeservers.com


Robert Dunn

unread,
Mar 16, 2000, 3:00:00 AM3/16/00
to
Hi, Damon.

Damon Chandler wrote:
>
> Ok, the first thing that you need to do is add a permanent margin to the
> RichEdit control. Specifically, you need to adjust the "working area" a.k.a.

<snip>

Wow, Damon. Very good. I would have wimped out and suggested using
more than one RE and trying to keep them synchronized. This is much
better (assuming that it works, of course <g>). Thanks.

robert


Damon Chandler

unread,
Mar 18, 2000, 3:00:00 AM3/18/00
to
Hi Robert,

> Wow, Damon. Very good. I would have wimped out and suggested using
> more than one RE and trying to keep them synchronized.

Thanks! I was thinking of using two controls at first as well, but I
decided to give the presented approach a shot.

zombacity

unread,
Mar 24, 2000, 3:00:00 AM3/24/00
to
But it would be much easier just to have another RE bordering the first at its LHS
and put the line count into each line of the second. Wouldn't it?? Question: do you
prefer easy practice or interesting theory?

Robert Dunn wrote:
> Wow Damon. Very good. I would have wimped out and suggested using

Robert Dunn

unread,
Mar 27, 2000, 3:00:00 AM3/27/00
to
Hi, Z.

zombacity wrote:
>
> But it would be much easier just to have another RE bordering the first at its LHS
> and put the line count into each line of the second. Wouldn't it?? Question: do you
> prefer easy practice or interesting theory?
>
> Robert Dunn wrote:

<snip>

Perhaps I am confused, Z.... Damon offered code that I thought was
both interesting (in theory) and a better solution than two RE controls
(LHS or RHS). Perhaps I missed your point?

robert


Robert Dunn

unread,
Apr 4, 2000, 3:00:00 AM4/4/00
to
Hello again, Z.

zombacity wrote:
>
> Damon's code is great...but IMHO needs quite a lot of work to understand and quite a lot
> of code lines to implement.
Perhaps Damon's solution is "a lot of code," but probably not much more
(maybe less) than synchronizing two RE controls. (That is much harder
than you would expect for pre-RE 3.0 controls.) And certainly his code
is far more straight-forward, IMHO.

robert


zombacity

unread,
Apr 5, 2000, 3:00:00 AM4/5/00
to
Damon's code is great...but IMHO needs quite a lot of work to understand and quite a lot
of code lines to implement. Maybe I have a simple mind.

zombacity

unread,
Apr 14, 2000, 3:00:00 AM4/14/00
to
There's some good stuff in Windows, once you manage to find it. How about:-
void __fastcall TForm11::RepRESelectionChange(TObject *Sender)
{ int TopLine1=RepRE->Perform(EM_GETFIRSTVISIBLELINE,0,0);
int TopLine2=RE2->Perform(EM_GETFIRSTVISIBLELINE,0,0);
int Diff = TopLine1-TopLine2;
RE2->Perform(EM_LINESCROLL,0,Diff);
}

Seems an easy way to synchronise the two REs.

OK, it needs some refinement to keep the fonts in line and make sure that RE2 ( the index RE)
has enough lines filled in with line numbers to keep up with RepRE (the data RE).


//-------

Damon Chandler (TeamB)

unread,
Apr 14, 2000, 3:00:00 AM4/14/00
to
zombacity wrote:
> There's some good stuff in Windows, once you manage to find it. How about:-
> void __fastcall TForm11::RepRESelectionChange(TObject *Sender)
> { int TopLine1=RepRE->Perform(EM_GETFIRSTVISIBLELINE,0,0);
> int TopLine2=RE2->Perform(EM_GETFIRSTVISIBLELINE,0,0);
> int Diff = TopLine1-TopLine2;
> RE2->Perform(EM_LINESCROLL,0,Diff);
> }
> Seems an easy way to synchronise the two REs.

No. What's happens when the user scrolls with the scroll bar, but the caret
position doesn't change. The line numbers don't update when the user enter's a
line break, or the text wraps to form a new line. What about OLE drag and
drop? By tapping into the WM_PAINT message, all of these cases are handled.
Robert is right, this is not a trivial task, and, FWIW, we are both familiar
with the relevant RE messages (Robert much more than I <g>).

--
Damon Chandler (TeamB)
http://bcbcaq.freeservers.com


zombacity

unread,
Apr 18, 2000, 3:00:00 AM4/18/00
to
However in my test (1) when user presses enter or return key (i.e. enters a line
break?) on the bottom line of RE1, then RE2 scrolls properly and (2) when I copy
some text from another document and paste it into the bottom line (i.e. OLE drop
and drag?) the same happens OK; also (3) I am not sure what is the best behaviour
regarding word wrap; it depends on the original requirement. If I am typing a long
text, I think I prefer the index to show the line numbers as on the screen.
Otherwise a long paragraph will just be indexed as a single line.
0 new messages