views:

410

answers:

4

I have a Delphi 2007 TRichEdit with several lines in it. I want to scroll the richedit vertically such that a specific line number if approximately centered in the visible/display area of the richedit. For example, I want to write the code for CenterLineInRichEdit in this example:

procedure CenterLineInRichEdit(Edit: TRichEdit; LineNum: Integer);
begin
  ...
  Edit.ScrollTo(...);
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  REdit: TRichEdit;
  i: Integer;
begin
  REdit := TRichEdit.Create(Self);
  REdit.Parent := Self;
  Redit.ScrollBars := ssVertical;
  REdit.SetBounds(10, 10, 200, 150);
  for i := 1 to 25 do
    REdit.Lines.Add('This is line number ' + IntToStr(i));
  CenterLineInRichEdit(REdit, 13);
end;

I looked into using the WM_VSCROLL message, and it allows scrolling up/down one line, etc. but not scrolling to center a specific line.

+1  A: 

Send an EM_LINESCROLL message to the RichEdit:

SendMessage(REdit.Handle, EM_LINESCROLL, 0, NumberOfVerticalLinesToScroll);

See the Delphi (link for D2007 help file, not web URL!) topic here.

Ken White
+1  A: 

You will need to use a couple of Windows messages to manipulate this aspect of your control in a generic fashion:

You will need to calculate how many lines to scroll up/down from the current top-line to bring a desired absolute line number into view, but you will have to calculate the number of lines visible in the control yourself (using font metrics and control height).

Note that with a RichEdit control the height of each line may vary according to fonts applied to the text in the control so any approach based on line numbers alone is likely to be only approximately accurate. Also I'm not sure that it's possible to determine the current visible range of the control (i.e. the number of lines currently visible) directly, so calculating it yourself is necessary.

From memory, the SynEdit control offers some additional control over such things, providing both a read/write TopLine property as well as a LinesInWindow property. However, I think SynEdit is not rich text capable, but if this is not actually a concern in your application (i.e. you can use a consistent font for all lines in the content) then it may be an attractive or suitable alternative.

Deltics
I looked over your ideas and posted a solution similar to this. Thanks!
Anagoge
+1  A: 

Give this a try;

procedure VertCenterLine(RichEdit: TRichEdit; LineNum: Integer);
// I don't know the reason but the RichEdit 2 control in VCL does not
// respond to the EM_SCROLLCARET in Richedit.h but it does so to the
// constant in WinUser.h
const
  EM_SCROLLCARET  = $00B7;
var
  TextPos: lResult;
  Pos: TSmallPoint;
begin
  TextPos := SendMessage(RichEdit.Handle, EM_LINEINDEX, LineNum, 0);

  if TextPos <> -1 then begin
    // Go to top
    SendMessage(RichEdit.Handle, EM_SETSEL, 0, 0);
    SendMessage(RichEdit.Handle, EM_SCROLLCARET, 0, 0);

    // Get the coordinates for the beginning of the line
    Longint(Pos) := SendMessage(RichEdit.Handle, EM_POSFROMCHAR, TextPos, 0);

    // Scroll from the top
    SendMessage(RichEdit.Handle, WM_VSCROLL,
        MakeWParam(SB_THUMBPOSITION, Pos.y - RichEdit.ClientHeight div 2), 0);

    // Optionally set the caret to the beginning of the line
    SendMessage(RichEdit.Handle, EM_SETSEL, TextPos, TextPos);
  end;
end;

The below is an alternative in that it centers the first occurance of a string instead of a line number;

procedure VertCenterText(RichEdit: TRichEdit; Text: string);
const
  EM_SCROLLCARET  = $00B7;
var
  FindText: TFindText;
  TextPos: lResult;
  Pos: TSmallPoint;
begin
  FindText.chrg.cpMin := 0;
  FindText.chrg.cpMax := -1;
  FindText.lpstrText := PChar(Text);
  TextPos := SendMessage(RichEdit.Handle, EM_FINDTEXT,
      FR_DOWN or FR_WHOLEWORD, Longint(@FindText));

  if TextPos <> -1 then begin
    SendMessage(RichEdit.Handle, EM_SETSEL, 0, 0);
    SendMessage(RichEdit.Handle, EM_SCROLLCARET, 0, 0);

    Longint(Pos) := SendMessage(RichEdit.Handle, EM_POSFROMCHAR, TextPos, 0);
    SendMessage(RichEdit.Handle, WM_VSCROLL,
        MakeWParam(SB_THUMBPOSITION, Pos.y - RichEdit.ClientHeight div 2), 0);

    SendMessage(RichEdit.Handle, EM_SETSEL, TextPos, TextPos);
  end;
end;
Sertac Akyuz
This solution works well. The only problem I found was when you try to focus a line at the top of the edit, Pos.y - RichEdit.ClientHeight div 2 could be negative, which is invalid for a Word type, so I added a Max(0, ...) wrapped around that statement and it works fine now. I also looked over the other suggestions here and independently came up with another solution, which is a bit simpler, but it assumes a constant line height throughout the document, and also that the font reports its height correctly, so it won't work for everyone.
Anagoge
A: 

Based on the ideas here, I came up with one solution. It assumes that all the lines in the richedit are the same height and that the richedit's default font correctly reports its height, but it might be useful to some people:

type
  TCustomEditHack = class(TCustomEdit);

procedure CenterLineInEdit(Edit: TCustomEdit; LineNum: Integer);
var
  VisibleLines: Integer;
  TopLine: Integer;
  FirstLine: Integer;
begin
  FirstLine := Edit.Perform(EM_GETFIRSTVISIBLELINE, 0, 0);
  VisibleLines := Round(Edit.ClientHeight / Abs(TCustomEditHack(Edit).Font.Height));

  if VisibleLines <= 1 then
    TopLine := LineNum
  else
    TopLine := Max(LineNum - Round((VisibleLines/2)) + 1, 0);

  if FirstLine <> TopLine then
    Edit.Perform(EM_LINESCROLL, 0, TopLine - FirstLine);
end;

I tested this with TRichEdit, but it might work for TMemo as well.

Anagoge