3

Need a component derived from TMemo (not TSyn components)

I need a line to the left(inside or outside) of a TMemo whose thickness(optional) and color can be controlled just for the purposes of indication. It need not be functional as a gutter but looks like one especially like that of a SynMemo as shown in the image. The problem with SynMemo is that it doesn't support variable width fonts like Tahoma but the TMemo does.

enter image description here

I tried making a few composite components with CustomContainersPack by combining a TShape with TMemo, even superimposing a TMemo on top of TSynMemo but didn't succeed as the paint while dragging made it look disassembled and CCPack is not that robust for my IDE.

KMemo, JvMemo and many other Torry.net components were installed and checked for any hidden support for achieving the same but none worked.

Grouping of components together is also not a solution for me since many mouse events are tied to the Memo and calls to FindVCLWindow will return changing components under the mouse. Furthermore many components will be required so grouping with TPanel will up the memory usage.

user30478
  • 347
  • 1
  • 3
  • 13
  • 3
    Little correction - this components does support true type fonts, but only **fixed-width** ones (like `Courier`, `Consolas` etc) – MBo Apr 29 '19 at 09:06
  • I have edited it thank you. – user30478 Apr 29 '19 at 09:53
  • TSynMemo and TSynEdit are, AFAIK, custom components and not based on the Windows-supplied edit controls, so there you can do anything you want. – Rudy Velthuis Apr 29 '19 at 09:54
  • @RudyVelthuis Putting support for Tahoma in Syn Controls is too difficult for me as a beginner in making Delphi Custom Components. It would be most welcome If someone could guide that way too since I see many people dissatisfied with Syn controls due to this specific restriction. That's why I asked for an easier alternative. – user30478 Apr 29 '19 at 09:58
  • 2
    AFAIK, you can use EM_SETRECT or one of the other EM_ messages to set the formatting rectangle. Just leave some space on the left and custom draw your gutter there. – Rudy Velthuis Apr 29 '19 at 09:59
  • 1
    *...for the purposes of indication.* Indication of what exactly? – Tom Brunberg Apr 29 '19 at 10:00
  • 1
    @user30478: I was not suggesting you change SynMemo, just explaining why it works there. **But see my other comment**. Not an answer, since I have no time to write up something, but a suggestion. Just override the Paint function, call inherited and add your own behaviour. – Rudy Velthuis Apr 29 '19 at 10:01
  • @TomBrunberg to indicate for example if the input is wrong i.e. form validation. – user30478 Apr 29 '19 at 10:03
  • Changing a complex fixed-character-width editor control to support variable-width fonts (without still using fixed columns) would likely be _very_ complicated. You would likely need to change thousands of lines of code and add thousands of new lines of code (and you'd need to know a lot about how the control is designed). – Andreas Rejbrand Apr 29 '19 at 10:03
  • 2
    If I were you, I might create a new custom control that contains a `TMemo` as a child control and draws a bar to the left of it. The edit control could have no frame (border), and you could draw your own edit-control-like frame around the new custom control. That's be easy. – Andreas Rejbrand Apr 29 '19 at 10:06
  • I agree with that Andreas. I have now indicated in the top that the solution is not related to TSynControls. – user30478 Apr 29 '19 at 10:07
  • Thanks, but how is form validation related to a `TMemo`? You write error info in the memo? Should the line move when scrolling memo? Or would the line be just an attention marker? – Tom Brunberg Apr 29 '19 at 10:07
  • @TomBrunberg Attention marker yes. Just that it is a simple colored shape. – user30478 Apr 29 '19 at 10:08
  • So, put a colored panel 5 pixel wide beside the memo. ??? – Tom Brunberg Apr 29 '19 at 10:09
  • @AndreasRejbrand I am looking for that _if I was you.._ answer from the outset. Don't know why I have not been able to express myself. – user30478 Apr 29 '19 at 10:10
  • @TomBrunberg sorry, I have entirely forgotten to state that I intend to have a lot of such controls and putting a shape besides each one is tedious and my controls can change coordinates too (from code). So need a control specifically. – user30478 Apr 29 '19 at 10:13
  • I no one else beats me to it (and I find the time to do it), I might write such a sample control tonight as an answer here. – Andreas Rejbrand Apr 29 '19 at 10:15
  • @AndreasRejbrand Honestly, I was thinking of you when I put the question. I have seen and loved your 'tag editor' component. I was wondering you are not so active these days so how could this problem be solved. – user30478 Apr 29 '19 at 10:17
  • Of course, neither of the duplicates combine a memo and a bar-type component, but the method would be exactly the same. – Tom Brunberg Apr 29 '19 at 10:40
  • @TomBrunberg I have now clarified regarding the problems with grouping that I faced. – user30478 Apr 29 '19 at 10:45
  • 1
    Oh well, you seem to come up with new requirements all the time. I need to wait for the next few additions, before I return to this one. You should have included all those requirements and findings from your previous work in your original question. – Tom Brunberg Apr 29 '19 at 11:17
  • 1
    @Andreas: Or a simple frame with a shape and a memo. The shape can be turned red or green or yellow or whatever, or just turned into the background colour of the form or transparent. Can be re-used, is less work and does not need to be installed, etc. – Rudy Velthuis Apr 29 '19 at 12:45

2 Answers2

6

You can use the WM_Paint message and a hack to do this without creating a new component, Otherwise create a descendant of TMemo and apply the same changes below

 TMemo = class(Vcl.StdCtrls.TMemo)
  private
    FSidecolor: TColor;
    FSideColorWidth: Integer;
    FAskForAttention: Boolean;
    procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
    procedure SetSideColorWidth(const Value: Integer);
    procedure SetSideColor(const Value: TColor);
    procedure SetAskForAttention(const Value: Boolean);
  published
    property SideColor: TColor read FSideColor write SetSideColor default clRed;
    property SideColorWidth: Integer read FSideColorWidth write SetSideColorWidth default 2;
    property AskForAttension: Boolean read FAskForAttention write SetAskForAttention;
  end;

{ TMemo }

procedure TMemo.SetAskForAttention(const Value: Boolean);
begin
  FAskForAttention := Value;
  Invalidate;
end;

procedure TMemo.SetSideColor(const Value: TColor);
begin
  FSideColor := Value;
  Invalidate;
end;

procedure TMemo.SetSideColorWidth(const Value: Integer);
begin
  FSideColorWidth := Value;
  Invalidate;
end;

procedure TMemo.WMPaint(var Message: TWMPaint);
var
  DC: HDC;
  Pen: HPen;
  R,G,B: Byte;
begin
  inherited;
  if FAskForAttention then
  begin
    DC := GetWindowDC(Handle);
    try
      B := Byte(FSidecolor);
      G := Byte(FSidecolor shr 8);
      R := Byte(FSidecolor shr 16);

      Pen := CreatePen(PS_SOLID, FSideColorWidth, RGB(R,G,B));
      SelectObject(DC, Pen);
      SetBkColor(DC, RGB(R,G,B));
      Rectangle(DC, 1, 1, FSideColorWidth, Height - 1);
      DeleteObject(Pen);
    finally
      ReleaseDC(Handle, DC);
    end;
  end;
end;

And you can use it like this

procedure TForm15.Button1Click(Sender: TObject);
begin
  memo1.SideColor := ColorBox1.Selected;
  memo1.SideColorWidth := 2;
  memo1.AskForAttension := True;
end;

and you get this result

enter image description here

Limitations:

As this is merely another hack to draw a simple rectangle on the side, do not expect it to be perfect on all situations. I did notice the following when testing:

  • If the border is too thick you get the following effect enter image description here
  • When on mouse move the line sometimes disappear and don't get painted (I think it is because of drawing focus rect).

Note: I see the guys in comments suggested to create a custom component with panel and memo put together, If you want to try this, take a look at my answer to

Creating a new components by combining two controls (TEdit and TTrackBar) in Delphi VCL

It is basically the same Ideas.


Edit:

Ok I took into consideration what is mentioned in comments and adapted my answer,

I also changed the way I'm getting the canvas of the component. The new implementation becomes this

{ TMemo }

procedure TMemo.SetAskForAttention(const Value: Boolean);
var
  FormatRect: TRect;
begin
  if FAskForAttention <> Value then
  begin
    FAskForAttention := Value;

    if not FAskForAttention then
    begin
      Perform(EM_SETRECT, 0, nil);
    end
    else
    begin
      FormatRect := GetClientRect;

      if IsRightToLeft then
        FormatRect.Right := FormatRect.Right - FSideColorWidth - 3
      else
        FormatRect.Left := FormatRect.Left + FSideColorWidth + 3;

      Perform(EM_SETRECT, 0, FormatRect);
    end;
    Invalidate;
  end;
end;

procedure TMemo.SetSideColor(const Value: TColor);
begin
  if FSideColor <> Value then
  begin
    FSideColor := Value;
    Invalidate;
  end;
end;

procedure TMemo.SetSideColorWidth(const Value: Integer);
var
  FormatRect: TRect;
begin
  if FSideColorWidth <> Value then
  begin
    FSideColorWidth := Value;
    FormatRect := GetClientRect;

    if IsRightToLeft then
      FormatRect.Right := FormatRect.Right - FSideColorWidth - 3
    else
      FormatRect.Left := FormatRect.Left + FSideColorWidth + 3;

    Perform(EM_SETRECT, 0, FormatRect);
  end;
end;

procedure TMemo.WMPaint(var Message: TWMPaint);
var
  Canvas: TControlCanvas;
  CRect: TRect;
begin
  inherited;
  if FAskForAttention then
  begin
    Canvas := TControlCanvas.Create;
    try
      Canvas.Control := Self;
      Canvas.Font.Assign(Self.Font);

      CRect := GetClientRect;

      if IsRightToLeft then
        CRect.Left := CRect.Right - FSideColorWidth
      else
        CRect.Width := FSideColorWidth;

      Canvas.Brush.Color := FSidecolor;
      Canvas.Brush.Style := bsSolid;
      Canvas.FillRect(CRect);
    finally
      Canvas.Free;
    end;
  end;
end;

There is no limitations for the size and it does not overlap the scrollbars.

Final result:

enter image description here

References I used to write this answer:

Nasreddine Galfout
  • 2,550
  • 2
  • 18
  • 36
  • 1
    There is no WM_PAIN message (ouch!), but I guess you meant WM_PAINT. – Rudy Velthuis Apr 29 '19 at 12:32
  • @RudyVelthuis ouch! it really hurts, thanks for the edit – Nasreddine Galfout Apr 29 '19 at 12:33
  • @NasreddineGalfout Even before I can read through to the end, I see that you have put a lot of effort in writing and formatting extensive answers in both the questions. A big thanks for putting your work out for the community. – user30478 Apr 29 '19 at 12:40
  • 2
    You've drawn in non-client area. That's why you had to use GetWindowDC. WM_NCPAINT should be the message for drawing in non-client area. Leaving that aside, I think the "left" in the question is your "right" actually, and drawing in client area handling WM_PAINT by making room with EM_SETRECT as Rudy mentioned should lead to a cleaner solution. – Sertac Akyuz Apr 29 '19 at 12:42
  • 1
    @SertacAkyuz I was trying with BidiMode right to left to see if I'm going to paint over the scroll bars. For the EM_SETRECT I did not know it existed before, I will try to adapt my answer to it. I was trying a combination of non-client and client painting and forgot to change the message (the invalidate method sends them both that why I did not notice the difference) – Nasreddine Galfout Apr 29 '19 at 12:59
  • @NasreddineGalfout the small width restriction is not of a big problem (for bright colors at least) but the focus rectangle adaptation will make this an excellent solution. A versatile clean singular component responding to mouse and FindVCLWindow is any-day better for me than grouped components. – user30478 Apr 29 '19 at 13:20
  • I would draw on the client area, left to the text (in your RTL case, to the right of the text). note you will need to handle other messages to invalidate the control. in any case you got my up-vote for a very nice starting point. BTW, you could use `ColorToRGB` and avoid the R,G,B mess. – kobik Apr 29 '19 at 13:39
  • @kobik I changed my approach I hope this rises to the same expectations you had – Nasreddine Galfout Apr 29 '19 at 14:39
  • 2
    Cool. the `case` statements seems unnecessary. you can use a simple `if IsRightToLeft then... else...`. and the message structure should be `TWMPaint` not `TWMNCPaint`. – kobik Apr 29 '19 at 14:53
  • 2
    One thing I'd suggest to the OP (answer is fine) is to leave the gutter at all times so that the text does not jump left-right when attention is required. – Sertac Akyuz Apr 29 '19 at 14:58
  • @NasreddineGalfou This component mirrors the TSynMemo. One reason not to miss the TSynMemo. Perfectly addresses the problem and I bet will be helpful to others. – user30478 Apr 29 '19 at 15:09
  • @user30478 good to know, I never used TSynMemo and this another reason not to :) – Nasreddine Galfout Apr 29 '19 at 15:12
  • 2
    @user30478 oh and just as a reminder the same thing here can be used with TEdit – Nasreddine Galfout Apr 29 '19 at 15:13
  • 2
    .. by using EM_SETMARGINS in place of EM_SETRECT. – Sertac Akyuz Apr 29 '19 at 21:58
4

Instead of writing a custom control, put a panel or a shape beside the standard memo and give it any colour you like.

If this is too tedious to repeat many times, then put the memo and the shape on a frame and put that in the repository. Set the anchors to make sure they resize correctly. You don't even need to write code for that and you have an instant "imitation custom control".

Much better and simpler than writing, installing and testing a custom control, IMO.

Now if you want to put text or numbers or icons in the gutter, then it would pay out to write a custom control. Use EM_SETRECT to set the internal formatting rectangle, and custom draw the gutter in the overridden Paint method. Do not forget to call inherited.

Rudy Velthuis
  • 28,387
  • 5
  • 46
  • 94
  • As I see it, I was not having the essential knowledge of Frames in Delphi. Can see that it has got design time advantages like CCBox. Is it possible to create run time instances of the same is my question and I am looking into it. – user30478 Apr 29 '19 at 13:09
  • @user30478 Yes you can but only in the same project you created them. You can not use the same frame in another project unless you create it again. – Nasreddine Galfout Apr 29 '19 at 13:14
  • @Nasreddine: you can add the same (frame) unit to another project too. So that is no big problem. – Rudy Velthuis Apr 29 '19 at 13:34
  • @RudyVelthuis you are right about that, it is just I prefer code more than visuals (it pays a lot when debugging). – Nasreddine Galfout Apr 29 '19 at 13:38
  • An important takeaway for me is this answer, almost opened up a new vista. **These Frames are like Fragments in Android**. – user30478 Apr 29 '19 at 15:12
  • 2
    @user30478: Frames predate Android, so it is probably the other way around. Just like I am not like my son but my son is like me. – Rudy Velthuis May 01 '19 at 18:31
  • @RudyVelthuis :D that's true. But, I did it on purpose so that a newer Delphi Developer can get an 'inkling' that these might be different from the otherwise static TPanel, TGroupbox etc. Android's terminology is little bit more suggestive here. – user30478 May 01 '19 at 19:45
  • @user30478: Actually, I prefer the Delphi term, "frame". "Fragment" sounds like you are dealing with a shard of something broken. – Rudy Velthuis May 01 '19 at 19:47