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.
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.
Send an EM_LINESCROLL message to the RichEdit:
SendMessage(REdit.Handle, EM_LINESCROLL, 0, NumberOfVerticalLinesToScroll);
See the EM_LINESCROLL MSDN topic.
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;
You will need to use a couple of Windows messages to manipulate this aspect of your control in a generic fashion:
EM_GETFIRSTVISIBLELINE to retrieve the current, topmost visible line number (0 based)
EM_LINESCROLL to scroll the text up/down by a specified number of lines
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.
Related
I am trying to save and then restore the vertical scroll position in RichEdit.
A global var to store scroll pos:
SI: TScrollInfo;
This code saves the scroll position:
FillChar( SI, SizeOf(SI), #0 );
SI.cbSize := SizeOf(SI);
SI.fMask := SIF_POS;
GetScrollInfo( RichEdit1.Handle, SB_VERT, SI );
This code tries to restore it:
RichEdit1.Perform( WM_VSCROLL, MakeLong(SB_THUMBTRACK, SI.nPos), 0 );
The text in RichEdit restores its older position OK. The problem is the vertical scrollbar won't jump to the older location.
My system: Win 7 64, Delphi 2009
What am I doing wrong?
Option 1
In many ways, the "cleanest" solution would be to use the EM_GETSCROLLPOS and EM_SETSCROLLPOS messages:
const
EM_GETSCROLLPOS = $04DD;
EM_SETSCROLLPOS = $04DE;
var
P: TPoint;
procedure TForm1.btnSaveClick(Sender: TObject);
begin
RichEdit1.Perform(EM_GETSCROLLPOS, 0, #P)
end;
procedure TForm1.btnRestoreClick(Sender: TObject);
begin
RichEdit1.Perform(EM_SETSCROLLPOS, 0, #P)
end;
However, beware of the 16-bit limitation described in the documentation, which limits the vertical range you are able to represent using these messages. If you display large RTF documents, this might be an issue (a showstopper, really).
Option 2
Actually, your initial approach seems (to my surprise) not to suffer from this limitation. You will lose in precision, not range. The problem you are observing with the scrollbar can be fixed by using SB_THUMBPOSITION instead of SB_THUMBTRACK.
Option 3
var
Y: Integer;
procedure TForm1.btnSaveClick(Sender: TObject);
begin
y := RichEdit1.Perform(EM_GETFIRSTVISIBLELINE, 0, 0);
end;
procedure TForm1.btnRestoreClick(Sender: TObject);
var
NewY: Integer;
begin
NewY := RichEdit1.Perform(EM_GETFIRSTVISIBLELINE, 0, 0);
RichEdit1.Perform(EM_LINESCROLL, 0, Y - NewY);
end;
might be a viable option.
I am developing an application on Embarcadero XE where i receive real time data from the ethernet port and display on a teechart chart on the screen.
The application works like an osciloscope, that is, there is a time window (10 seconds for example) of data that the chart displays, and each new incoming point overwrites what already is on screen.
I would like your help to make a code that puts a marker on only the newest point added, so the user can keep track of which of the points on the screen is the most recent point. I don´t want all of the points with a marker, i want only the newest.
The series being used is a fastline.
Here is the code i'm using to add data to the chart:
//Delete already existing point
if (Oscilografia.Series[0].Count>1) then
begin
Oscilografia.Series[0].Delete(cont);
end;
//Write point
Oscilografia.Series[0].addxy(cont,data, '', clblue);
You have several options. The simplest is to make a new TPointSeries to display the current point. If you wish to not show this series in the legend then simply set :
Oscilografia.Series[n].ShowInLegend := false;
where n is the index of the series you wish to exclude from the legend.
Alternatively, you can custom-draw any relevant items in the OnAfterDraw handler. For example :
procedure TForm1.Chart1AfterDraw(Sender: TObject);
var
xPos, yPos : integer;
begin
Chart1.Canvas.Pen.Color := clRed;
Chart1.Canvas.Pen.Style := psSolid;
Chart1.Canvas.Pen.Width := 1;
Chart1.Canvas.Pen.Mode := pmCopy;
xPos := Chart1.BottomAxis.CalcPosValue(CurrentXValue);
yPos := Chart1.LeftAxis.CalcPosValue(CurrentYValue);
// Parameters are
// X-Coord, Y-Coord, X-Radius, Y-Radius, Start Angle, End Angle, Hole%
Chart1.Canvas.Donut(xPos, yPos, 3, 3, 0, 360, 0);
end;
This produces, for example :
Custom drawing lets you do other things also, like add markers, etc. For example :
procedure TForm1.Chart1AfterDraw(Sender: TObject);
var
xPos, yPos : integer;
yMax, yMin : integer;
begin
Chart1.Canvas.Pen.Color := clRed;
Chart1.Canvas.Pen.Style := psSolid;
Chart1.Canvas.Pen.Width := 1;
Chart1.Canvas.Pen.Mode := pmCopy;
xPos := Chart1.BottomAxis.CalcPosValue(CurrentXValue);
yPos := Chart1.LeftAxis.CalcPosValue(CurrentYValue);
Chart1.Canvas.Donut(xPos, yPos, 3, 3, 0, 360, 0);
Chart1.Canvas.Pen.Color := clGreen;
Chart1.Canvas.Pen.Style := psDash;
yMax := Chart1.LeftAxis.CalcPosValue(Chart1.LeftAxis.Maximum);
yMin := Chart1.LeftAxis.CalcPosValue(Chart1.LeftAxis.Minimum);
Chart1.Canvas.DoVertLine(xPos, yMax, yMin);
end;
Which gives a dashed vertical line that follows the current point :
Note that the CalcPosValue function is exposed by the chart axes and allows you to translate a point in the axis-space to an integer (screen) coordinate in the chart's canvas space.
As an alternative to J's proposal of using custom drawing techniques to draw a pointer, you could change the TFastLineSeries to use a TLineSeries and make its Pointer Visible. Then, you can use OnGetPointerStyle event to hide all the pointers except the last one:
uses Series;
var Series1: TLineSeries;
procedure TForm1.FormCreate(Sender: TObject);
var i: Integer;
begin
Chart1.View3D:=false;
Series1:=Chart1.AddSeries(TLineSeries) as TLineSeries;
for i:=0 to 360 do
Series1.Add(Sin(PI*i/180));
Series1.Pointer.Visible:=true;
Series1.OnGetPointerStyle:=SeriesGetPointerStyle;
end;
function TForm1.SeriesGetPointerStyle(Sender:TChartSeries; ValueIndex:Integer):TSeriesPointerStyle;
begin
result:=(Sender as TLineSeries).Pointer.Style;
if (ValueIndex<>Sender.Count-1) then
result:=psNothing;
end;
And as a complement, if you want to show the Mark of the last point in the series, you can make the series' Marks Visible and use OnGetMarkText event to hide all the Marks except the last one:
uses Series;
var Series1: TLineSeries;
procedure TForm1.FormCreate(Sender: TObject);
var i: Integer;
begin
Chart1.View3D:=false;
Series1:=Chart1.AddSeries(TLineSeries) as TLineSeries;
for i:=0 to 360 do
Series1.Add(Sin(PI*i/180));
Series1.Pointer.Visible:=true;
Series1.OnGetPointerStyle:=SeriesGetPointerStyle;
Series1.Marks.Visible:=true;
Series1.OnGetMarkText:=SeriesGetMarkText;
end;
function TForm1.SeriesGetPointerStyle(Sender:TChartSeries; ValueIndex:Integer):TSeriesPointerStyle;
begin
result:=(Sender as TLineSeries).Pointer.Style;
if (ValueIndex<>Sender.Count-1) then
result:=psNothing;
end;
procedure TForm1.SeriesGetMarkText(Sender:TChartSeries; ValueIndex:Integer; var MarkText:String);
begin
if (ValueIndex<>Sender.Count-1) then
MarkText:='';
end;
Note I'm using a TLineSeries here also, but if you are only interested on showing the Marks and not the Pointer, you can still use a TFastLineSeries.
I'm trying to implement an RTF-capable tool tip window in Delphi XE. To render the rich text, I'm using an off-screen TRichEdit. I need to do two things:
Measure the size of the text.
Paint the text
To accomplish both tasks, I wrote this method:
procedure TLookupHintWindow.CallFormatRange(R: TRect; var Range: TFormatRange;
MustPaint: Boolean);
var
TextRect: TRect;
begin
RichText.SetBounds(R.Left, R.Top, R.Right, R.Bottom);
TextRect := Rect(0, 0,
RichText.Width * Screen.Pixelsperinch,
RichText.Height * Screen.Pixelsperinch);
ZeroMemory(#Range, SizeOf(Range));
Range.hdc := Canvas.Handle;
Range.hdcTarget := Canvas.Handle;
Range.rc := TextRect;
Range.rcpage := TextRect;
Range.chrg.cpMin := 0;
Range.chrg.cpMax := -1;
SendMessage(RichText.Handle, EM_FORMATRANGE,
NativeInt(MustPaint), NativeInt(#Range));
SendMessage(RichText.Handle, EM_FORMATRANGE, 0, 0);
end;
The Range parameter is passed in, so I can use the calculated dimensions outside this method. The MustPaint parameter determines if the range should be calculated (False) or painted (True).
To calculate the range, I call this method:
function TLookupHintWindow.CalcRichTextRect(R: TRect; const Rtf: string): TRect;
var
Range: TFormatRange;
begin
LoadRichText(Rtf);
CallFormatRange(R, Range, False);
Result := Range.rcpage;
Result.Right := Result.Right div Screen.PixelsPerInch;
Result.Bottom := Result.Bottom div Screen.PixelsPerInch;
// In my example yields this rect: (0, 0, 438, 212)
end;
To paint it:
procedure TLookupHintWindow.DrawRichText(const Text: string; R: TRect);
var
Range: TFormatRange;
begin
CallFormatRange(R, Range, True);
end;
The problem is that while it calculates a rectangle that is 438 pixels wide and 212 high, it actually paints one that is very wide (gets clipped) and only 52 pixels high.
I have word wrap turned on, although it was my impression that that should not be needed.
Any ideas?
Your units are off. Consider this expression from your code, for example:
RichText.Width * Screen.Pixelsperinch
The left term is in pixels, and the right term is in pixels/inch, so the units of the result are pixels²/inch. The expected unit for the rectangles used in em_FormatRange is twips. If you want to convert pixels to twips, you need this:
const
TwipsPerInch = 1440;
RichText.Width / Screen.PixelsPerInch * TwipsPerInch
You don't need an off-screen rich-edit control. You just need a windowless rich-edit control, which you can instruct to paint directly onto your tool-tip. I've published some Delphi code that makes the basics straightforward. Beware that it's not Unicode-aware, and I have no plans to make it so (although it might not be too complicated to do).
The main function from my code is DrawRTF, shown below, in RTFPaint.pas. It doesn't quite fit your needs, though; you want to discover the size before drawing it, whereas my code assume you already know the dimensions of the drawing target. To measure the size of the RTF text, call ITextServices.TxGetNaturalSize.
Word wrapping is important. Without it, the control will assume it has infinite width to work with, and it will only start a new line when the RTF text requests it.
procedure DrawRTF(Canvas: TCanvas; const RTF: string; const Rect: TRect;
const Transparent, WordWrap: Boolean);
var
Host: ITextHost;
Unknown: IUnknown;
Services: ITextServices;
HostImpl: TTextHostImpl;
Stream: TEditStream;
Cookie: TCookie;
res: Integer;
begin
HostImpl := TDrawRTFTextHost.Create(Rect, Transparent, WordWrap);
Host := CreateTextHost(HostImpl);
OleCheck(CreateTextServices(nil, Host, Unknown));
Services := Unknown as ITextServices;
Unknown := nil;
PatchTextServices(Services);
Cookie.dwCount := 0;
Cookie.dwSize := Length(RTF);
Cookie.Text := PChar(RTF);
Stream.dwCookie := Integer(#Cookie);
Stream.dwError := 0;
Stream.pfnCallback := EditStreamInCallback;
OleCheck(Services.TxSendMessage(em_StreamIn, sf_RTF or sff_PlainRTF,
lParam(#Stream), res));
OleCheck(Services.TxDraw(dvAspect_Content, 0, nil, nil, Canvas.Handle,
0, Rect, PRect(nil)^, PRect(nil)^, nil, 0, txtView_Inactive));
Services := nil;
Host := nil;
end;
In Delphi i wish to draw text inside a TRect. I am hoping for the following functionality:
Draw the text centred vertically within the TRect
Draw the text centred horizontally within the TRect
If there is space for more than 1 line of text (using TRect's height), draw the text multiline
If the text does not fit in the TRect (either on a single or mult line) then append ellipsis to the text.
I can see the Windows.DrawText() function almost covers this functionality, however when writing text, multiline and vertically centred are mutually exclusive.
I was wondering if this functionality is built into windows (2000+)? If not is there a way to do this without writing my own function?
Sorry, this is a combination of all previous answers and comments. But it seems OP needs more assistance.
function DrawTextCentered(Canvas: TCanvas; const R: TRect; S: String): Integer;
var
DrawRect: TRect;
DrawFlags: Cardinal;
DrawParams: TDrawTextParams;
begin
DrawRect := R;
DrawFlags := DT_END_ELLIPSIS or DT_NOPREFIX or DT_WORDBREAK or
DT_EDITCONTROL or DT_CENTER;
DrawText(Canvas.Handle, PChar(S), -1, DrawRect, DrawFlags or DT_CALCRECT);
DrawRect.Right := R.Right;
if DrawRect.Bottom < R.Bottom then
OffsetRect(DrawRect, 0, (R.Bottom - DrawRect.Bottom) div 2)
else
DrawRect.Bottom := R.Bottom;
ZeroMemory(#DrawParams, SizeOf(DrawParams));
DrawParams.cbSize := SizeOf(DrawParams);
DrawTextEx(Canvas.Handle, PChar(S), -1, DrawRect, DrawFlags, #DrawParams);
Result := DrawParams.uiLengthDrawn;
end;
procedure TForm1.FormPaint(Sender: TObject);
const
S = 'This is a very long text as test case for my paint routine.';
var
R: TRect;
begin
SetRect(R, 100, 100, 200, 140);
Canvas.Rectangle(R);
InflateRect(R, -1, -1);
Caption := Format('%d characters drawn', [DrawTextCentered(Canvas, R, S)]);
end;
Measure the text first using DT_CALCRECT. Pass DT_WORDBREAK to specify that word wrapping is enabled. This will allow you to find the required height for your text. Then you can, in your code, calculate the vertical offset that gives you vertically centred text, and draw to that offset.
I'm porting some very old code from Delph7 to Delphi2010 with a few changes as possible to the existing code base for the usual reasons.
First: the good news for anyone who hasn't jumped yet: it's not as daunting as it may look! I'm actually pleased (& surprised) at how easy 1,000,000+ lines of code have moved across. And what a relief to be back on the leading edge! Delphi 2010 has so many great enhancements.
However, I'm having a cosmetic problem with some TStringGrids and TDbGrids descendants.
In the last century (literally!) someone wrote the two methods below.
The first method is used to justify text. When run in Delphi 2010, the new text and the unjustified text to both appear in the cells written to. Of course it's a mess visually, almost illegible. Sometimes, as a result of the second method is use, the grid cells are actually semi-transparent, with text from the window below showing through. (Again, not pretty!)
It appears to me that Delphi 2010's TDbGrid and TStringGrid have some differences in the way they handle transparency?
I haven't much experience in this area of Delphi (in fact, I have no idea what the 2nd method is actually doing!) and was hoping someone could give me some pointers on what's going on and how to fix it.
TIA!
Method 1
procedure TForm1.gridDrawCell(Sender: TObject; Col, Row: Integer;
Rect: TRect; State: TGridDrawState);
{Used to align text in cells.}
var
x: integer;
begin
if (Row > 0) AND (Col > 0) then
begin
SetTextAlign(grdTotals.Canvas.Handle, TA_RIGHT);
x := Rect.Right - 2;
end
else
begin
SetTextAlign(grdTotals.Canvas.Handle, TA_CENTER);
x := (Rect.Left + Rect.Right) div 2;
end;
grdTotals.Canvas.TextRect(Rect, x, Rect.Top+2, grdTotals.Cells[Col,Row]);
end;
Method 2
procedure WriteText(ACanvas: TCanvas; ARect: TRect; DX, DY: Integer; const Text: string;
TitleBreak: TTitleBreak; Alignment: TAlignment);
const
AlignFlags: array [TAlignment] of Integer = (DT_LEFT or
{ DT_WORDBREAK or } DT_EXPANDTABS or DT_NOPREFIX, DT_RIGHT or
{ DT_WORDBREAK or } DT_EXPANDTABS or DT_NOPREFIX, DT_CENTER or
{ DT_WORDBREAK or } DT_EXPANDTABS or DT_NOPREFIX);
var
ABitmap: TBitmap;
AdjustBy: Integer;
B, R: TRect;
WordBreak: Integer;
begin
WordBreak := 0;
if (TitleBreak = tbAlways) or ((TitleBreak = tbDetect) and (Pos(Chr(13) + Chr(10), Text) = 0))
then
WordBreak := DT_WORDBREAK;
ABitmap := TBitmap.Create;
try
ABitmap.Canvas.Lock;
try
AdjustBy := 1;
if (Alignment = taRightJustify) then
Inc(AdjustBy);
with ABitmap, ARect do
begin
Width := Max(Width, Right - Left);
Height := Max(Height, Bottom - Top);
R := Rect(DX, DY, Right - Left - AdjustBy, Bottom - Top - 1); { ### }
B := Rect(0, 0, Right - Left, Bottom - Top);
end;
with ABitmap.Canvas do
begin
Font := ACanvas.Font;
Brush := ACanvas.Brush;
Brush.Style := bsSolid;
FillRect(B);
SetBkMode(Handle, TRANSPARENT);
DrawText(Handle, PChar(Text), Length(Text), R, AlignFlags[Alignment] or WordBreak);
end;
ACanvas.CopyRect(ARect, ABitmap.Canvas, B);
finally
ABitmap.Canvas.Unlock;
end;
finally
ABitmap.Free;
end;
end;
In Method 2, I would try with SetBkMode(Handle, OPAQUE);
Update: and I would put it before FillRect(B)
We always use the DrawText function that gives us control on alignment (vert and hor).
You have to use a FillRect(Rect) before to clean up the content.
I've never used SetBkMode() but my guess is you can go without that.
I'm posting this as an answer (which it's not) so I can include an image.
Thanks for your suggestion. Using OPAQUE helped with initial writing to the TDbGrid. Backgrounds don't bleed through anymore! I'm a bit embarrassed I hadn't spotted the "TRANSPARENT" term before.
However, changes to cells are still failing to erase previous contents, so they look like the screen below. Darn!
The grid contents were moved down one row, but the also remain in the cell above in which they were previously.