2

I would like to have a TMemo which will always begin with the string 'SELECT c_name FROM ' and I want to lock it so users cannot remove or replace this string in the TMemo, they will have to write their text AFTER this string. Can someone help me with that ? I tried something with the onChange event but the problem is that users can click at the begining of the TMemo and edit it by the begining.

I'm using Delphi 6.

JensG
  • 13,148
  • 4
  • 45
  • 55
Moussamoa
  • 417
  • 1
  • 4
  • 19
  • This is not possible with `TMemo`. – David Heffernan Dec 11 '15 at 15:44
  • Why is it impossible with TMemo? Can't we use SelStart or something else? – Moussamoa Dec 11 '15 at 15:48
  • 2
    Don't make life difficult for yourself. Just put two TMemos below each other (touching) with border bsNone, and make the top one read-only. – Jan Doggen Dec 11 '15 at 16:11
  • 1
    It just is not possible. The control doesn't support partial read only. Yes you can change the text back every time the user changes it, but it will look quite revolting and I don't believe you will make it work robustly. Why don't you ask how to solve your problem rather than asking how to implement your solution? – David Heffernan Dec 11 '15 at 16:27
  • What @jandoggen said: Don't put the "select ..." in the memo in the first place. And how are you ever going to protect the result from Sql Injection, never mind the support headaches you'll be making for yourself with users' home-rolled Sql? Better to get a 3rd party library that allows a Sql query to be built graphically - see e.g. http://devtools.korzh.com/easyquery/ – MartynA Dec 11 '15 at 17:43
  • It's not possible with `TMemo` because the control does not support it. It also would be confusing to users. It would be better to put the non-editable first line in either a separate, disabled `TEdit` above the memo, or put it in a `TLabel` above the memo. Both methods accomplish your goal of not allowing them to be edited, in a way that makes it much more clear to the user what is happening and avoids all of the lunacy of trying to make an editable control only partially editable. – Ken White Dec 11 '15 at 17:57
  • @KenWhite: I can imagine one scenario where that separate would not be desirable - if the user wants to edit the SQL statement and then copy/paste the whole thing somewhere else. If the `SELECT` is separated into a `TEdit`, it would require multiple copy/paste operations. For a `TLabel`, a button or menu item would have to be provided to concat the pieces together and put them on the clipboard as a single string. In any case, `TRichEdit` supports what the OP is asking for. – Remy Lebeau Dec 11 '15 at 18:13
  • Yes, I wanted to allow to copy/paste the SQL statement, that is why I asked for TMemo. SQL injection is not a problem here, I know that my users (coworkers) will not try to break my work. – Moussamoa Dec 21 '15 at 08:32

2 Answers2

10

What you are asking for is not possible with TMemo. It is just a thin wrapper around a standard Win32 EDIT control, which does not support this kind of functionality.

You need to use TRichEdit instead. It supports protecting text like you have described. After adding the desired text, select it using the TRichEdit.SelStart and TRichEdit.SelLength properties and then set the TRichEdit.SelAttributes.Protected property to true. If the user tries to modify the protected text in any way, TRichEdit will reject the modification by default (you can override that decision by using the TRichEdit.OnProtectChange event to set the AllowChange parameter to true). For example:

RichEdit1.Text := 'SELECT c_name FROM ';
RichEdit1.SelStart := 0;
RichEdit1.SelLength := 19;
RichEdit1.SelAttributes.Protected := True;

Update: by default, protecting the text also prevents the user from copying it. If you want the user to be able to copy the protected text, you have to explicitly enable that. Although TRichEdit has a OnProtectChange event to allow access to protected text, it does not tell you WHY the text is requesting access. However, the underlying EN_PROTECTED notification does. So, you can subclass the TRichEdit to intercept EN_PROTECTED directly, and then return 0 (allow access) only when the user is copying the protected text.

interface

uses
  ...;

type
  TMyForm = class(TForm)
    RichEdit1: TRichEdit;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    DefRichEditWndProc: TWndMethod;
    procedure RichEditWndProc(var Message: TMessage);
  public
    { Public declarations }
  end;

...

implementation

uses
  Richedit;

procedure TMyForm.FormCreate(Sender: TObject);
begin
  DefRichEditWndProc := RichEdit1.WindowProc;
  RichEdit1.WindowProc := RichEditWndProc;

  RichEdit1.Text := 'SELECT c_name FROM ';
  RichEdit1.SelStart := 0;
  RichEdit1.SelLength := 19;
  RichEdit1.SelAttributes.Protected := True;
end;

procedure TMyForm.RichEditWndProc(var Message: TMessage);
begin
  DefRichEditWndProc(Message);
  if Message.Msg = CN_NOTIFY then
  begin
    if TWMNotify(Message).NMHdr.code = EN_PROTECTED then
    begin
      if PENProtected(Message.lParam).Msg = WM_COPY then
        Message.Result := 0;
    end;
  end;
end;

Alternatively:

interface

uses
  ...;

type
  TRichEdit = class(ComCtrls.TRichEdit)
  private
    procedure CNNotify(var Message: TWMNotify); message CN_NOTIFY;
  end;

  TMyForm = class(TForm)
    RichEdit1: TRichEdit;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

...

implementation

uses
  Richedit;

procedure TMyForm.FormCreate(Sender: TObject);
begin
  RichEdit1.Text := 'SELECT c_name FROM ';
  RichEdit1.SelStart := 0;
  RichEdit1.SelLength := 19;
  RichEdit1.SelAttributes.Protected := True;
end;

procedure TRichEdit.CNNotify(var Message: TWMNotify);
begin
  inherited;
  if Message.NMHdr.code = EN_PROTECTED then
  begin
    if PENProtected(Message.NMHdr).Msg = WM_COPY then
      Message.Result := 0;
  end;
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • This is definitely the best answer as it allows locking of any text part whether it is in the beginning or in the middle of any line. And even more it allows you to lock multiple text parts if needed. Now combine this with the ability to change text format and you can easily make intuitive user interface. – SilverWarior Dec 11 '15 at 22:13
  • It works just like I wanted, thanks. I just found a little issue about this solution: I cannot copy the text on Ctrl-C when I select all the text, but I think this is normal because a part of the text is locked. – Moussamoa Dec 21 '15 at 08:31
  • 1
    It is possible to allow protected text to be copied. It takes an extra step to enable. I have updated my answer with an example. – Remy Lebeau Dec 21 '15 at 21:34
  • Do you know how to disable the protection temporarily? I tried RichEdit1.SelAttributes.Protected := False; but it does not work. Thanks again for your precious help. – Moussamoa Dec 22 '15 at 08:38
  • I found how to disable the protection temporarily: I created a boolean variable bDisable that I set to True when I want to disable the protection and to False when I want to re-enable it, then in the OnProtectionChanged event I set AllowChange to bDisable. – Moussamoa Dec 22 '15 at 09:05
  • That works. `SelAttribues.Protected` also works, you just have to select the protected text with `SelStart`/`SelLength` first. – Remy Lebeau Dec 22 '15 at 16:46
2

Well it is possible, but I don't know, if you like the solution.

As already said TMemo has not that that kind of behaviour implemented. So you have to program this behaviour.

Use the application OnIdle event and a memento for the content. On each Idle message validate memo content. If the memo content did not start with 'SELECT c_name FROM ' then assign the memento value, otherwise store the memo content to memento.

Here is an example for that validation and cursor protection

type
  TMainForm = class( TForm )
    Memo1: TMemo;
    ApplicationEvents1: TApplicationEvents; // OnIdle = ApplicationEvents1Idle
    procedure ApplicationEvents1Idle( Sender: TObject; var Done: Boolean );
  private
    FMemo1StartWith: string;
    FMemo1Memento  : string;
    procedure ValidateMemo( AMemo: TMemo; const AStartWith: string; var AMemento: string );
  public
    procedure AfterConstruction; override;

  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.AfterConstruction;
begin
  inherited;
  FMemo1StartWith := 'SELECT c_name FROM ';
end;

procedure TMainForm.ApplicationEvents1Idle( Sender: TObject; var Done: Boolean );
begin
  ValidateMemo( Memo1, FMemo1StartWith, FMemo1Memento );
end;

procedure TMainForm.ValidateMemo( AMemo: TMemo; const AStartWith: string; var AMemento: string );
var
  lCurrentContent: string;
begin
  // protect content

  lCurrentContent := AMemo.Text;
  if Pos( AStartWith, lCurrentContent ) = 1
  then
    AMemento := lCurrentContent
  else if Pos( AStartWith, AMemento ) = 1
  then
    AMemo.Text := AMemento
  else
    AMemo.Text := AStartWith;

  // protect cursor position

  if ( AMemo.SelLength = 0 ) and ( AMemo.SelStart < Length( AStartWith ) )
  then
    AMemo.SelStart := Length( AStartWith );
end;
Sir Rufo
  • 18,395
  • 2
  • 39
  • 73
  • @SilverWarior Did you test it? I did and there were no problems with multiple lines or any flickering – Sir Rufo Dec 11 '15 at 22:09
  • No I haven't tested it before. I did it now and it does indeed works. I'm must admit that I don't exactly understand how it works. Any way I'm deleting my previous comment. – SilverWarior Dec 11 '15 at 22:21