5

I have a form displaying data from a database, It has a few buttons and several panels. The panels contain a variety of components, specifically TEdits, TComboBox, TDateTimePicker, TCheckBox, TListBox and TstringGrid.

When the form is opened such that the user can view, but not edit, the data I currently disable all the components except for the buttons using

for i := 0 to FrmAddNewMember.ComponentCount-1 do
    if FrmAddNewMember.Components[i] is TPanel then
      (FrmAddNewMember.Components[ i ] as TPanel).enabled := false;

This works fine except that I would now like the user to be able to copy to the clipboard the text in the TEdits, the date in the TDateTimePicker, the selected item in the TComboBox etc etc. but still not change it.

I altered the code to the following which sets the TEdits to Read Only. This does what I want for TEdits but the other type of controls do not have a read only property, so I disabled them as before.

for i := 0 to FrmAddNewMember.ComponentCount-1 do
    if not (FrmAddNewMember.Components[i] is TButton) then //(keep buttons working)
       case FrmAddNewMember.Components[i] of
          TEdit: (FrmAddNewMember.Components[ i ] as TEdit).readonly := true; //allows copying but not editing
          TComboBox: (FrmAddNewMember.Components[ i ] as TComboBox).enabled := false;  //no read only propert?
          TDateTimePicker: (FrmAddNewMember.Components[ i ] as TDateTimePicker).enabled := false;  //ditto
          TCheckBox: (FrmAddNewMember.Components[ i ] as TCheckBox).enabled := false;
          TListBox:  (FrmAddNewMember.Components[ i ] as TListBox).enabled := false;
          TstringGrid: (FrmAddNewMember.Components[ i ] as TstringGrid).enabled := false;
       end;

Is there a way to make the other controls non editable but still allow their contents to be copied to the clipboard?

PS I have looked at

disable-edits-on-datagridview-but-still-allow-for-highlighting-to-copy-and-paste

and

make-all-controls-on-a-form-read-only-at-once-without-one-linkbutton

and searched elsewhere. Maybe it cannot be done simply.

9
  • Isn't that small edit to my title a little pedantic if you don't also attempt to answer the question? I put the word Delphi there on purpose as I am fed up searching the web for questions/answers involving Delphi and finding some SO ones that look relevant but then turn out to be for a different language. SO may use the tags but Google doesn't. Commented Oct 30, 2018 at 10:58
  • Why aren't you using database conrols like TField, TDateTimeField, TMemoField, TBooleanField, etc. All of them have Readonly property to control whether they can be changed or not Commented Oct 30, 2018 at 12:31
  • "Why aren't you using database controls... " Because the exe is a front end to a remote MySQL database (and is used by several people). The form does a great deal of processing and much of what it displays is processed or formatted data from the database. It is not merely a simple 'window' on the raw database data. Commented Oct 30, 2018 at 13:47
  • Strange design to disable component and allow to copy data from it. Commented Oct 30, 2018 at 14:33
  • @Dima The controls are all disabled in this view as I use the same layout for both editing and viewing (view has the controls disabled to prevent accidental changes). It's handy for the user to be able to copy things like an email address or reference number to use elsewhere. This can be done from the 'editing' form but there is a risk of altering the data, which would surprise the user when they closed the form and got a message asking them if they want to save changes. Hence I wanted to also allow such copying of data from the safer 'view' form where all the controls are disabled. Commented Oct 30, 2018 at 17:23

3 Answers 3

3

I think, you can emulate a popup menu for your components (because standard popup menu will not work for disabled ones).

But if you will have popup menu for the form and FormMouseDown event handler, you can analyze where mouse pointer is (under which component, I mean) and call popup with Copy menu item.

Quick example for listboxes:

unit Unit6;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.Menus, Vcl.StdCtrls, Vcl.ExtCtrls, Clipbrd;

type
  TForm6 = class(TForm)
    Panel1: TPanel;
    ListBox1: TListBox;
    ListBox2: TListBox;
    PopupMenu1: TPopupMenu;
    miCopy: TMenuItem;
    procedure miCopyClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
  private
    { Private declarations }
    selectedText: string;
  public
    { Public declarations }
  end;

var
  Form6: TForm6;

implementation

{$R *.dfm}

procedure TForm6.FormCreate(Sender: TObject);
begin
  ListBox1.ItemIndex := 1;
  ListBox2.ItemIndex := 1;
  Panel1.OnMouseDown := FormMouseDown;
end;

procedure TForm6.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  i, parentX, parentY: integer;
  p: TPoint;
  lb: TListBox;
begin
  if Button <> mbRight then
    exit;

  selectedText := '';
  for i := 0 to ComponentCount - 1 do
    if Components[i] is TListBox then
    begin
      lb := TListBox(Components[i]);
      begin
        p := lb.ParentToClient(Point(X, Y));
        if lb.ClientRect.Contains(p) then
        begin
          parentX := 0;
          parentY := 0;
          if Assigned(lb.Parent) then
          begin
            parentX := lb.Parent.ClientOrigin.X;
            parentY := lb.Parent.ClientOrigin.Y;
          end;

          if lb.ItemIndex > -1 then
          begin
            selectedText := lb.Items[lb.ItemIndex];
            PopupMenu1.Popup(lb.Left + parentX + p.X, lb.Top + parentY + p.Y);
          end;
          break;
        end;
      end;
    end;
end;

procedure TForm6.miCopyClick(Sender: TObject);
begin
  if selectedText = '' then
    exit;

  Clipboard.AsText := selectedText;
end;

end.

Here ListBox1 is placed on the TPanel component. Please note you should assign form's OnMouseDown handler to all your panels or other containers. Also, if you have nested containers, you need use recursive algorithm to find parentX, parentY.

7
  • Ah, that sounds like an interesting idea. I was thinking more along the lines of using Ctrl C and shortcuts like that but a proper pop up menu with just Copy on it would do the job. Any hints on implementation? Commented Oct 30, 2018 at 13:34
  • I'm not sure you can realize work with shortcuts because disabled controls doesn't get focus.
    – Miamy
    Commented Oct 30, 2018 at 14:13
  • Good answer (didn't test it though) but it might be world of pain to adapt that code for every needed component as they have different data properties (TListBox and TDateTmePicker as example). Commented Oct 30, 2018 at 14:36
  • @Dima, not too much, I hope. Some controls can be casted to their parent, and we can use Text property.
    – Miamy
    Commented Oct 30, 2018 at 14:46
  • TListBox and TStringGrid will return empty string after their casting. By the way Text property is protected, so OP needs to use non-trivial casting to get access to Text property. I hope he knows what he is doing)) Commented Oct 30, 2018 at 14:55
0

You can use this code to make combobox read-only. Also you can use the same approach to other edits if you can obtain the window handle of the edit.

procedure MakeComboboxReadOnly(const ACombobox: TCombobox);
var cbi: TComboBoxInfo;
begin
    cbi.cbSize := SizeOf(cbi);
    GetComboBoxInfo(ACombobox.Handle, cbi);
    SendMessage(cbi.hwndItem, EM_SETREADONLY, 1, 0);
end;
3
  • 1
    This code prevents user custom input only, but a combobox still allows to select items from its list.
    – Miamy
    Commented Oct 30, 2018 at 15:05
  • To prevent user from selecting another item - set combobox Style := csSimple
    – dwrbudr
    Commented Oct 30, 2018 at 18:35
  • or csDropDownList. I know, but it is not acceptable for all tasks.
    – Miamy
    Commented Oct 30, 2018 at 18:51
0

Just to add a closure to my own question. The way I did it in the end was to simply place speed buttons next to the controls containing the text I wanted to copy and used the normal

uses ClipBrd;
...
Clipboard.AsText := MyControl1.text;

to copy the data.

The trick though was not to place the speed buttons as children of the panel containing the control, (which is disabled, hence also disabling all the controls inside it) but instead to just place them on the form and then move them so they are positioned in front of the panel next to the relevant control.

That way the speed buttons look like they are part of the panel yet will still operate when the panel and all it's controls are disabled.

Not the answer you're looking for? Browse other questions tagged or ask your own question.