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;
Related
I want to draw on a canvas a word vertically and next to it a word horizontally.
I used a old suggestion like this :
in the maiForm's create event :
GetObject(MainForm.Font.Handle,SizeOf(TLogFont),#LogFont);
NewLogFont := LogFont;
NewLogFont.lfEscapement := 900;
NewFont := CreateFontIndirect(NewLogFont);
OldFont := MainForm.Font.Handle;
where
LogFont,NewLogFont : TLogFont;
NewFont,OldFont : HFont;
and in drawing routine :
fontTemp := TFont.Create;
fontTemp.Assign(aCanvas.Font);
......
aCanvas.Font.Handle := newFont; // if i coment this line the two strings drawn verically else both drawn horizonatlly
aCanvas.Font.Size := 8;
h := textHeight('1');
aCanvas.textOut(x,y,aString);
aCanvas.Font.Assign(fontTemp);
aCanvas.textOut(x+20,y,bString);
.....
fontTemp.Free;
In my old application (D2007) it worked ok but in Delphi 10.2, the change of orientation (from vert to horiz) changes both strings to horiz.
Any help please ?
No, as you said it is not an absolutely rare code. This approach lets you rotate text without using VCL's canvas properties.
Pure WinAPI for output text with rotation
The code below uses no VCL's capabilities to output rotated text onto provided device context (HDC).
procedure TForm1.DrawTextRotatedA(ADC: HDC; AFontHandle: HFONT;
Angle, X, Y: Integer; AColor: COLORREF; AText: String);
var
LogFont: tagLOGFONT;
OldFontHandle: HFONT;
NewFontHandle: HFONT;
begin
if (ADC = 0) or (AFontHandle = 0) then
Exit;
if GetObject(AFontHandle, SizeOf(LogFont), #LogFont) = 0 then
Exit;
// Set color of text and its rotation angle
SetTextColor(ADC, AColor);
if Angle > 360 then
Angle := 0;
LogFont.lfEscapement := Angle * 10;
LogFont.lfCharset := 1;
LogFont.lfOutPrecision := OUT_TT_PRECIS;
LogFont.lfQuality := PROOF_QUALITY;
// Create new font
NewFontHandle := CreateFontIndirect(LogFont);
try
OldFontHandle := SelectObject(ADC, NewFontHandle);
try
// Output result
SetBKMode(ADC, TRANSPARENT);
try
TextOut(ADC, X, Y, LPCWSTR(AText), Length(AText));
finally
SetBKMode(ADC, OPAQUE);
end;
finally
// Restore font handle
NewFontHandle := SelectObject(ADC, OldFontHandle);
end;
finally
// Delete font handle
DeleteObject(NewFontHandle);
end;
end;
There are places for improvements but this is just an example to prove you are wrong calling such a code rare. This example expects HFONT as one of arguments to perform all actions over it. You probably could get font handle from TControl by using WM_GETFONT message, but most of VCL's components don't honor this message (it works, f.e. with TListView which returns correct font handle). Trying to get font handle from HDC returns System font that doesn't support rotation at all. Perhaps I did something wrong but I have acted accordingly to microsoft.docs.
Using VCL for output text with rotation
I didn't get what code you have provide in your question should to do (it is cannot be compiled) so I rewrite it to show you how to output rotated text with using VCL's capabilities.
procedure TForm1.DrawTextRotatedB(ACanvas: TCanvas; Angle, X, Y: Integer;
ATextColor: TColor; AText: String);
var
NewX: Integer;
NewY: integer;
Escapement: Integer;
LogFont: TLogFont;
NewFontHandle: HFONT;
OldFontHandle: HFONT;
begin
if not Assigned(ACanvas) then
Exit;
// Get handle of font and prepare escapement
GetObject(ACanvas.Font.Handle, SizeOf(LogFont), #LogFont);
if Angle > 360 then
Angle := 0;
Escapement := Angle * 10;
// We must initialise all fields of the record structure
LogFont.lfWidth := 0;
LogFont.lfHeight := ACanvas.Font.Height;
LogFont.lfEscapement := Escapement;
LogFont.lfOrientation := 0;
if fsBold in ACanvas.Font.Style then
LogFont.lfWeight := FW_BOLD
else
LogFont.lfWeight := FW_NORMAL;
LogFont.lfItalic := Byte(fsItalic in ACanvas.Font.Style);
LogFont.lfUnderline := Byte(fsUnderline in ACanvas.Font.Style);
LogFont.lfStrikeOut := Byte(fsStrikeOut in ACanvas.Font.Style);
LogFont.lfCharSet := ACanvas.Font.Charset;
LogFont.lfOutPrecision := OUT_DEFAULT_PRECIS;
LogFont.lfClipPrecision := CLIP_DEFAULT_PRECIS;
LogFont.lfQuality := DEFAULT_QUALITY;
LogFont.lfPitchAndFamily := DEFAULT_PITCH;
StrPCopy(LogFont.lfFaceName, ACanvas.Font.Name);
// Create new font with rotation
NewFontHandle := CreateFontIndirect(LogFont);
try
// Set color of text
ACanvas.Font.Color := ATextColor;
// Select the new font into the canvas
OldFontHandle := SelectObject(ACanvas.Handle, NewFontHandle);
try
// Output result
ACanvas.Brush.Style := VCL.Graphics.bsClear;
try
ACanvas.TextOut(X, Y, AText);
finally
ACanvas.Brush.Style := VCL.Graphics.bsSolid;
end;
finally
// Restore font handle
NewFontHandle := SelectObject(ACanvas.Handle, OldFontHandle);
end;
finally
// Delete the deselected font object
DeleteObject(NewFontHandle);
end;
end;
Using case
Here is the code showing how to use procedures for rotating text.
procedure TForm1.aButton1Click(Sender: TObject);
var
DC: HDC;
begin
Repaint;
DC := GetDC(Handle);
try
DrawTextRotatedA(DC, Canvas.Font.Handle, TrackBar1.Position, 100, 100, clNavy, 'String');
finally
ReleaseDC(Handle, DC);
end;
DrawTextRotatedB(Canvas, TrackBar1.Position, 200, 100, clNavy, 'String');
end;
Sometimes it is faster to output rotated text onto DC without VCL. This could be useful if you are trying to deal with control that have no access to canvas. F.e. if you will try to paint tooltip (tooltip_class32) in your own style you probably might want to use the first method to output text (rotated or not).
Information
Here are links from docs.microsoft. they describe how and why one or another function was used.
About Device Contexts
TextOutW function
SetTextColor function
tagLOGFONTW structure
GetObject function
WM_GETFONT message
It's simple!
TFont has the property orientation that does the work! All this stuf I used is absolutely rare.
In Delphi 10 Seattle, I need to insert an image into an ImageList. The image is in a descendant of TGraphicControl (see source code below). The insertion seems to work. However, I get only a white rectangle in the ImageList:
function InsertCloudImageIntoImageList(AdvCloudImage1: TAdvCloudImage): Integer;
// TAdvCloudImage = class(TGraphicControl)
// WebPicture is TCloudPicture = class(TGraphic)
var
TempBitmap: TBitmap;
R: TRect;
begin
Result := 0;
TempBitmap := TBitmap.Create;
try
TempBitmap.SetSize(16, 16);
R.Width := 16;
R.Height := 16;
R.Top := 0;
R.Left := 0;
AdvCloudImage1.WebPicture.Draw(TempBitmap.Canvas, R);
Result := Form1.ImageList1.Add(TempBitmap, nil);
finally
TempBitmap.Free;
end;
end;
I suspect the bug is in the drawing on the bitmap canvas?
The correct way to draw here is to call Draw on the destination bitmap's canvas, passing the source graphic. The method you call is declared protected in TGraphic which indicates that you are not meant to call it from consumer code.
So instead of
AdvCloudImage1.WebPicture.Draw(TempBitmap.Canvas, R);
You should use
TempBitmap.Canvas.Draw(0, 0, AdvCloudImage1.WebPicture);
This greatly simplifies the function since you no longer need the TRect variable. Furthermore, there's no point assigning to Result more than once. The entire function can be:
function InsertCloudImageIntoImageList(AdvCloudImage1: TAdvCloudImage): Integer;
var
TempBitmap: TBitmap;
begin
TempBitmap := TBitmap.Create;
try
TempBitmap.SetSize(16, 16);
TempBitmap.Canvas.Draw(0, 0, AdvCloudImage1.WebPicture);
Result := Form1.ImageList1.Add(TempBitmap, nil);
finally
TempBitmap.Free;
end;
end;
The code below should be creating a bitmap that is a 48x48 rectangle, of blue background color and a Text (actually just a letter) centered horizontally and vertically of white color.
However nothing happens.
procedure MakeCustomIcon(AText: string; AWidth: Integer; AHeight: Integer; AColor: TAlphaColor; var ABlob: TBlob);
var
Bitmap: TBitmap;
Rect: TRectF;
InStream: TMemoryStream;
begin
Bitmap := TBitmap.Create;
InStream := TMemoryStream.Create;
try
Bitmap.SetSize(AWidth, AHeight);
Bitmap.Canvas.Clear(AColor);
Bitmap.Canvas.Stroke.Kind := TBrushKind.bkSolid;
Bitmap.Canvas.StrokeThickness := 1;
Bitmap.Canvas.Fill.Color := TAlphaColorRec.White;
Bitmap.Canvas.BeginScene;
Rect.Create(0, 0, AWidth, AHeight);
Bitmap.Canvas.FillText(Rect, AText, true, 100, [TFillTextFlag.ftRightToLeft], TTextAlign.taCenter, TTextAlign.taCenter);
Bitmap.Canvas.EndScene;
Bitmap.SaveToStream(InStream);
InStream.Position := 0;
ABlob.Clear;
ABlob.LoadFromStream(InStream);
finally
Bitmap.Free;
InStream.Free;
end;
I have tested the rest of my program to make sure the image (that Blob) is actually transporting and getting displayed, and it is doing so. The problem is fully contained on the way it is drawn the bitmap on the method above.
This TBlob is an array of byte.
I am looking to do rectangles like this below, to be used in TListView:
I have prepared a project.
1-) Write Text on TImage
2-) Draw on TImage
3-) Effect to TImage
I Try on XE5
Samples:
procedure ReDraw(Image: TImage);
var
MyRect: TRectF;
begin
if Image.Bitmap.IsEmpty then Exit;
MyRect := TRectF.Create(0, Ozellik.SeritTop, Image.Bitmap.Width, Ozellik.SeritBot);
with Image.Bitmap.Canvas do
begin
BeginScene;
if not Seffaf.IsChecked then
Fill.Color := Ozellik.SeritRenk
else
Fill.Color := TAlphaColorRec.Null;
FillRect(MyRect, 0, 0, [], 1);
Fill.Color := Ozellik.YaziRenk;
if FontCombo.ItemIndex <> -1 then
Font.Family := FontCombo.Items[FontCombo.ItemIndex];
Font.Size := Ozellik.YaziBoyut;
FillText(MyRect,FonYazi.Text.Trim,True,1,[],TTextAlign.taCenter,TTextAlign.taCenter);
EndScene;
end;
Image.Repaint;
end;
http://www.dosya.tc/server32/vHsbaC/CapsYapMasa_st_.rar.html
All canvas drawings must be grouped into a BeginScene/EndScene block. Also, it is recommended to draw within a try-finally block.
So, instead of
Bitmap.Canvas.Clear(AColor);
...
Bitmap.Canvas.BeginScene;
...
Bitmap.Canvas.EndScene;
you should do:
Bitmap.Canvas.BeginScene;
try
Bitmap.Canvas.Clear(AColor);
...
finally
Bitmap.Canvas.EndScene;
end;
-- Regards
I have an array of TImages each one containing thumbnail of a Image file in a specified directory and their Hint property set to their Image Filename for printing purpose.
all files are located on a remote server in a shared directory (Example: \192.168.1.50\imgscan\12-14-54\ *.jpg).
also each Image has a corresponding TCheckBox that users can check to mark images for printing.
I use the following code for printing (variable images_index holds the number of images in the selected directory)...
procedure PrintSelectedImages;
var
i: integer;
R1, R2: TRect;
Picture: TPicture;
Bitmap: TBitmap;
Total, done: integer;
begin
Total := 0;
done := 0;
for i := 0 to images_index - 1 do
if Checks[i].Checked then
INC(Total);
if Total = 0 then
begin
MessageDlg('No Images Selected!', mtInformation, [mbOK], 0);
Exit;
end;
Printer.BeginDoc;
if PrintDialog1.Execute then
begin
for i := 0 to images_index - 1 do
begin
if Checks[i].Checked then
begin
try
Picture := TPicture.Create;
Picture.LoadFromFile(images[i].Hint);
Bitmap := TBitmap.Create;
try
Bitmap.Width := Picture.Width;
Bitmap.Height := Picture.Height;
Bitmap.Canvas.Draw(0, 0, Picture.Graphic);
R1 := Rect(0, 0, Bitmap.Width, Bitmap.Height);
R2 := Rect(0, 0, Printer.PageWidth, Printer.PageHeight);
Printer.Canvas.CopyRect(R2, Bitmap.Canvas, R1);
INC(done);
finally
Bitmap.Free;
end;
finally
Picture.Free;
end;
if done < Total then
Printer.NewPage;
end; // if
end; // i
end; // if
Printer.EndDoc;
end;
Now...
On Microsoft XPS Document Writer I have no problems and all the pages are printed fine, but on real printers most of the time white papers come out and sometimes only some of the selected images are printed (for example 4 of 10 selected files).
What is the problem with my code? i googled a lot and found nothing!
Thanks.
The Canvas CopyRect function uses StretchBLT. We have had better results using the DIBits functions SetDIBitsToDevice or StretchDIBits. Here is our draw code. We have a DrawParams struct that is passed in with the details on how this image should be drawn.
The code below is using a TBitmap32 from graphics32. We use that because of some other drawing and resize routines we find useful. But the same code will work with a normal TBitmap.
{ TDrawParamsRecord }
TDrawParamsRecord = record
private
function GetHeight(): integer;
function GetWidth(): integer;
public
PictureZoom: integer;
Stretch: boolean;
Center: boolean;
KeepAspectRatio: boolean;
OutputRect: TRect;
ResizeMode: TResizeMode;
property Height: integer read GetHeight;
property Width: integer read GetWidth;
function Equal(OtherParams: TDrawParamsRecord): boolean;
end;
{
TCFImage.OutputToCanvas
---------------------------------------------------------------------------
When writing to the canvas we could have a Screen canvas, a metafile canvas
used to create a PDF file, or a printer canvas. Because of this we want to
make sure we are using the DIBits functions. Many printer drivers can't use
the StretchBLT function because of color space changes. Everyone should
support StretchDIBits.
When resizing the image we sometimes will resize it internally to match the
output size and other times we will let StretchDIBits handle the conversion.
}
procedure TCFImage.OutputToCanvas(Canvas: TCanvas; Image: TBitmap32; DrawParams: TDrawParamsRecord);
var
// StretchDIBits has BmpInfo passed in as a Var parameter so we can't
// use the read only property.
BmpInfo: TBitmapInfo;
begin
BmpInfo := Image.BitmapInfo;
// If th output matches the current image size then we can just move the bits,
// no reason for "Stretch"
if (DrawParams.Height = Image.Height) and (DrawParams.Width = Image.Width) then
begin
SetDIBitsToDevice(Canvas.Handle,
DrawParams.OutputRect.Left, DrawParams.OutputRect.Top,
DrawParams.Width, DrawParams.Height,
0, 0, 0, Image.Height, Image.Bits, BmpInfo, DIB_RGB_COLORS);
end
else
begin
StretchDIBits(Canvas.Handle,
DrawParams.OutputRect.Left, DrawParams.OutputRect.Top,
DrawParams.Width, DrawParams.Height,
0, 0, Image.Width, Image.Height,
Image.Bits, BmpInfo, DIB_RGB_COLORS, SRCCOPY);
end;
end;
If I wanted to move / shift the pixels of a bitmap how could I do so?
procedure MovePixels(Bitmap: TBitmap; Horizontal, Vertical: Integer);
begin
{ move the Bitmap pixels to new position }
end;
Example:
By calling MovePixels(Image1.Picture.Bitmap, 20, 20) for example would output like so:
It would be useful to also specify / change the color of the canvas that is left showing after moving the pixels. So in this example that gray / brown color could be blue etc.
I noticed there is Bitmap.Canvas.Pixels and Bitmap.Canvas.MoveTo properties, is this what I would need to do this?
I really don't know and I bet it is so simple..
You can't easily move pixels, but you can make a copy.
var
Source, Dest: TRect;
....
Source := Rect(0, 0, Bitmap.Width, Bitmap.Height);
Dest := Source;
Dest.Offset(X, Y);
Bitmap.Canvas.CopyRect(Dest, Bitmap.Canvas, Source);
What remains is to fill in the space with the colour of your choice which I am sure you can do easily enough with a couple of calls to FillRect.
However, I think that it would be simpler not to attempt this in-place. Instead I would create a new bitmap. Perhaps like this:
function CreateMovedImage(Bitmap: TBitmap; X, Y: Integer; BackColor: TColor): TBitmap;
var
Source, Dest: TRect;
begin
Source := Rect(0, 0, Bitmap.Width, Bitmap.Height);
Dest := Source;
Dest.Offset(X, Y);
Result := TBitmap.Create;
Try
Result.SetSize(Bitmap.Width, Bitmap.Height);
Result.Canvas.Brush.Style := bsSolid;
Result.Canvas.Brush.Color := BackColor;
Result.Canvas.FillRect(Source);
Result.Canvas.CopyRect(Dest, Bitmap.Canvas, Source);
Except
Result.Free;
raise;
End;
end;