如何使Delphi窗体上的所有控件不可编辑/更改,但仍允许用户复制内容

5

我有一个显示数据库数据的表单,其中包含几个按钮和多个面板。这些面板包含各种组件,特别是TEdits、TComboBox、TDateTimePicker、TCheckBox、TListBox和TstringGrid。

当用户以只读模式打开表单时,我会禁用除按钮外的所有组件,使用以下代码:

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

这个功能很好用,但我希望用户现在能够将TEdits中的文本、TDateTimePicker中的日期、TComboBox中的选定项等复制到剪贴板,但仍不能更改它们。

我修改了代码,将TEdits设置为只读。这对于TEdits实现了我想要的效果,但是其他类型的控件没有只读属性,所以我像之前一样禁用了它们。

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;

有没有办法使其他控件不可编辑,但仍然允许将它们的内容复制到剪贴板中?

附注 我已经查看了

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

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

并在其他地方搜索。也许这不能简单地完成。


为什么你不使用像TField、TDateTimeField、TMemoField、TBooleanField等数据库控件呢?它们都有只读属性来控制是否可以更改。 - SilverWarior
奇怪的设计可以禁用组件并允许从中复制数据。 - Josef Švejk
@Dima 在这个视图中,所有的控件都被禁用了,因为我在编辑和查看时使用相同的布局(查看时控件被禁用以防止意外更改)。用户可以方便地复制诸如电子邮件地址或参考编号之类的内容以在其他地方使用。这可以从“编辑”表单中完成,但存在更改数据的风险,当用户关闭表单并收到询问是否保存更改的消息时,会让用户感到惊讶。因此,我希望还可以允许从更安全的“查看”表单中复制数据。 - user2834566
@Dima:完全不是这样。如果您想禁用编辑但仍要使值可用且可复制,那么这是有意义的。这就是为什么TEdit的ReadOnly会是一个好的解决方案,但并非所有控件都支持它。 - Rudy Velthuis
@RudyVelthuis 我同意禁用组件以限制编辑(就像您的示例中一样),但不同意它的真正禁用(TPanel.Enabled := false所做的)。个人而言,如果我要编写这样的程序,我会创建自己的组件(派生自标准组件),具有特殊属性,可以正确地禁用它们并保持从它们复制信息的能力。当然,这种方式会花费一些时间(和金钱),但对于进一步的程序维护来说会更好。 - Josef Švejk
显示剩余4条评论
3个回答

3

我认为,您可以为组件模拟一个弹出菜单(因为标准的弹出菜单对于禁用的组件不起作用)。

但是如果您要为窗体创建弹出菜单并使用FormMouseDown事件处理程序,您可以分析鼠标指针所在的位置(我的意思是在哪个组件下),并调用带有Copy菜单项的弹出菜单。

以下是ListBox的快速示例:

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.

这里的ListBox1被放置在TPanel组件上。请注意,您应该将表单的OnMouseDown处理程序分配给所有面板或其他容器。此外,如果您有嵌套的容器,则需要使用递归算法来查找parentX,parentY


啊,那听起来是一个有趣的想法。我之前在考虑使用 Ctrl C 这样的快捷键,但是一个合适的只有“复制”选项的弹出菜单也可以完成任务。您对实现有什么线索吗? - user2834566
我不确定您能否通过快捷键实现工作,因为禁用的控件无法获得焦点。 - Miamy
好的回答(虽然没有测试过),但是将那段代码适应每一个所需组件可能会非常困难,因为它们具有不同的数据属性(例如TListBoxTDateTimePicker)。 - Josef Švejk
在进行类型转换后,TListBoxTStringGrid 将返回空字符串。另外,Text 属性是protected的,所以操作者需要使用非平凡的类型转换才能访问Text属性。我希望他知道自己在做什么)) - Josef Švejk
谢谢,是的,我对自己在做什么有一个合理的想法。只是没有考虑过用这种方式来实现。我原本以为必须将表单的OnMouseDown处理程序分配给所有面板才能捕获它,但幸运的是只有一层嵌套,所以不需要递归(尽管在我的经验中,递归解决方案通常简单而优雅),我会接受这个答案,因为它让我离我想要做的事情更近了一步。谢谢Miamy。 - user2834566
显示剩余2条评论

0

您可以使用此代码使组合框只读。如果您可以获取编辑的窗口句柄,也可以使用相同的方法来处理其他编辑。

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;

1
此代码仅防止用户自定义输入,但组合框仍允许从其列表中选择项目。 - Miamy
为防止用户选择其他项 - 设置combobox样式:= csSimple - dwrbudr
或者是csDropDownList。我知道,但它并不适用于所有任务。 - Miamy

0
只是为了给我的问题添加一个结束语。最后我做的方法就是在包含我想要复制的文本的控件旁边放置速度按钮,然后使用正常的


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

将数据复制。 但是,诀窍在于不将速度按钮放置在包含控件的面板下(该面板被禁用,因此也禁用了其中的所有控件),而是将它们放置在窗体上,并移动它们以使其位于与相关控件相邻的面板前方。 这样,速度按钮看起来就像是面板的一部分,但当面板及其所有控件被禁用时,仍然可以操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接