我可以更改监视列表中字符串的显示格式吗?

Can I change the display format for strings in the watch list?

我时不时地使用手表 window 来显示包含 sql 语句的字符串。

现在我select从上下文菜单中复制值并获取

'SELECT NAME FROM SAMPLE_TABLE WHERE  FIRST_NAME = ''George'''#$D#$A

当然,如果我想在显示结果的 sql 工具中执行它,则必须重新格式化此语句。这有点烦人。

有什么技巧/解决方法吗?

我认为通过在 IDE 中添加一些东西来尝试找出一种方法来做到这一点会很有趣,主要是因为当你发布你的问题时,我不知道如何做。事实证明,您可以使用包含如下单元的自定义 OTA 包轻松完成此操作。

顺便说一句,我特别感谢 Rob Kennedy 在另一个 SO 问题中指出 IDE 和其他对象一样有一个 Screen 对象。这提供了解决问题的简单方法,绕过了 OTA 接口的迷宫,我通常不得不使用它来编写 IDE 加载项。

它的工作原理是

  • 找到 Watch Window,

  • 在上下文菜单中找到 Copy Watch value 项并在其后添加一个新菜单项

  • 使用新项目的 OnClick 处理程序从 Watch Window 的焦点项目中获取值,根据需要重新格式化,然后将其粘贴到 Clipboard.

就使用 OTA 服务而言,它并没有做任何花哨的事情,但是 IDE 我认为 KISS 原则适用。

代码:

unit IdeMenuProcessing;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ToolsAPI, Menus, ClipBrd, ComCtrls;

type
  TOtaMenuForm = class(TForm)
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    OurMenuItem : TMenuItem;
    WatchWindow : TForm;
    WWListView : TListView;
    procedure GetWatchValue(Sender : TObject);
  end;

var
  OtaMenuForm: TOtaMenuForm;

procedure Register;

implementation

{$R *.dfm}

procedure ShowMenus;
begin
  OtaMenuForm := TOtaMenuForm.Create(Nil);
  OtaMenuForm.Show;
end;

procedure Register;
begin
  ShowMenus;
end;

procedure TOtaMenuForm.FormCreate(Sender: TObject);
var
  i : Integer;
  S : String;
  PM : TPopUpMenu;
  Item : TMenuItem;
begin

  // First create a menu item to insert in the Watch Window's context menu
  OurMenuItem := TMenuItem.Create(Self);
  OurMenuItem.OnClick := GetWatchValue;
  OurMenuItem.Caption := 'Get processed watch value';

  WatchWindow := Nil;
  WWListView := Nil;

  //  Next, iterate the IDE's forms to find the Watch Window
  for i := 0 to Screen.FormCount - 1 do begin
    S := Screen.Forms[i].Name;
    if CompareText(S, 'WatchWindow') = 0 then begin  // < Localize if necessary
      WatchWindow := Screen.Forms[i];
      Break;
    end;
  end;

  Assert(WatchWindow <> Nil);

  if WatchWindow <> Nil then begin
    //  Next, scan the Watch Window's context menu to find the existing "Copy watch value" entry
    //  and insert our menu iem after it
    PM := WatchWindow.PopUpMenu;
    for i:= 0 to PM.Items.Count - 1 do begin
      Item := PM.Items[i];
      if CompareText('Copy Watch &Value', Item.Caption) = 0 then begin // < Localize if necessary
        PM.Items.Insert(i + 1, OurMenuItem);
        Break;
      end;
    end;

    //  Now, find the TListView in the Watch Window
    for i := 0 to WatchWindow.ComponentCount - 1 do begin
      if WatchWindow.Components[i] is TListView then begin
        WWListView := WatchWindow.Components[i] as TListView;
        Break;
      end;
    end;
    Assert(WWListView <> Nil);
  end;
end;

procedure TOtaMenuForm.GetWatchValue(Sender: TObject);
var
  WatchValue : String;
begin
  //  This is called when the Watch Window menu item we added is clicked
  if WWListView.ItemFocused = Nil then begin
    Memo1.Lines.Add('no Watch selected');
    exit;
  end;
  WatchValue := WWListView.ItemFocused.SubItems[0];
  WatchValue := StringReplace(WatchValue, #$D#$A, ' ', [rfreplaceAll]);
  if WatchValue[1] = '''' then
    Delete(WatchValue, 1, 1);

  if WatchValue[Length(WatchValue)] = '''' then
    WatchValue := Copy(WatchValue, 1, Length(WatchValue) - 1);
  // [etc]  
  ClipBoard.AsText := WatchValue;
  Memo1.Lines.Add('>' +  WatchValue + '<');
end;

initialization

finalization
  if Assigned(OTAMenuForm) then begin
    OTAMenuForm.Close;
    FreeAndNil(OTAMenuForm);
  end;
end.

顺便说一句,我在 D7 中写了这个,因为我将它用作 SO 答案的一种最低公分母,因为很明显这里有很多人仍在使用它。以后的版本有额外的字符串函数,比如评论中提到的AniDequotedStr,这可能有助于重新格式化watch值。

更新: 根据 OP,以上不适用于 XE3,因为手表 window 是使用 TVirtualStringTree 而不是 TListView 实现的。我使用 ListView 的原因是我发现从剪贴板中拾取 Watch 值(在模拟单击上下文菜单的 Copy Watch Value 之后)来处理它不是很可靠。这似乎在 XE4 中有所改进(我没有 XE3 可以测试),所以这是一个 似乎 在 XE4 中工作的版本:

更新 #2: OP 提到,当 Delphi 首次启动时,以下代码的先前版本未能通过 WatchWindow <> Nil 断言。我想原因是在 IDE 中创建 Watch Window 之前调用了代码。我重新安排了代码并添加了一个 OTANotifier,用于获取项目桌面已加载的通知,广告使用它来调用新的 SetUp 例程。

unit IdeMenuProcessing;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ToolsAPI, Menus, ClipBrd, ComCtrls;

type
  TIdeNotifier = class(TNotifierObject, IOTANotifier, IOTAIDENotifier)
  protected
    procedure AfterCompile(Succeeded: Boolean);
    procedure BeforeCompile(const Project: IOTAProject; var Cancel: Boolean);
    procedure FileNotification(NotifyCode: TOTAFileNotification;
      const FileName: string; var Cancel: Boolean);
  end;

  TOtaMenuForm = class(TForm)
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    IsSetUp : Boolean;
    ExistingMenuItem,
    OurMenuItem : TMenuItem;
    WatchWindow : TForm;
    Services: IOTAServices;
    Notifier : TIdeNotifier;
    NotifierIndex: Integer;
    procedure GetWatchValue(Sender : TObject);
    procedure SetUp;
  end;

var
  OtaMenuForm: TOtaMenuForm;

procedure Register;

implementation

{$R *.dfm}

procedure ShowMenus;
begin
  OtaMenuForm := TOtaMenuForm.Create(Nil);
  OtaMenuForm.Services := BorlandIDEServices as IOTAServices;
  OtaMenuForm.NotifierIndex := OtaMenuForm.Services.AddNotifier(TIdeNotifier.Create);
  OtaMenuForm.Show;
end;

procedure Register;
begin
  ShowMenus;
end;

procedure TOtaMenuForm.SetUp;
var
  i : Integer;
  S : String;
  PM : TPopUpMenu;
  Item : TMenuItem;
begin
  if IsSetUp then exit;

  // First create a menu item to insert in the Watch Window's context menu
  OurMenuItem := TMenuItem.Create(Self);
  OurMenuItem.OnClick := GetWatchValue;
  OurMenuItem.Caption := 'Get processed watch value';

  WatchWindow := Nil;

  //  Next, iterate the IDE's forms to find the Watch Window
  for i := 0 to Screen.FormCount - 1 do begin
    S := Screen.Forms[i].Name;
    if CompareText(S, 'WatchWindow') = 0 then begin
      WatchWindow := Screen.Forms[i];
      Break;
    end;
  end;

  Assert(WatchWindow <> Nil);

  if WatchWindow <> Nil then begin
    //  Next, scan the Watch Window's context menu to find the existing "Copy watch value" entry
    //  and insert our menu item after it
    PM := WatchWindow.PopUpMenu;
    for i:= 0 to PM.Items.Count - 1 do begin
      Item := PM.Items[i];
      if CompareText('Copy Watch &Value', Item.Caption) = 0 then begin
        ExistingMenuItem := Item;
        PM.Items.Insert(i + 1, OurMenuItem);
        if ExistingMenuItem.Action <> Nil then
          Memo1.Lines.Add('Has action')
        else
          Memo1.Lines.Add('No action');
        Break;
      end;
    end;
  end;
  Caption := 'Setup complete';
  IsSetUp := True;
end;

procedure TOtaMenuForm.FormCreate(Sender: TObject);
begin
  IsSetUp := False;
end;

procedure TOtaMenuForm.GetWatchValue(Sender: TObject);
var
  S,
  WatchValue : String;
  TL : TStringList;
  i : Integer;
begin
  //  This is called when the Watch Window menu item we added is clicked

  ExistingMenuItem.Click;

  WatchValue := ClipBoard.AsText;
  WatchValue := StringReplace(WatchValue, '#$D#$A', #$D#$A, [rfreplaceAll]);

  if WatchValue <> '' then begin
    TL := TStringList.Create;
    try
      TL.Text := WatchValue;
      WatchValue := '';
      for i := 0 to TL.Count - 1 do begin
        S := TL[i];
        if S[1] = '''' then
          Delete(S, 1, 1);
        if S[Length(S)] = '''' then
          S := Copy(S, 1, Length(S) - 1);
         if WatchValue <> '' then
           WatchValue := WatchValue + ' ';
         WatchValue := WatchValue + S;
      end;
    finally
      TL.Free;
    end;
    // [etc]
  end;

  ClipBoard.AsText := WatchValue;
  Memo1.Lines.Add('>' +  WatchValue + '<');
end;

{ TIdeNotifier }

procedure TIdeNotifier.AfterCompile(Succeeded: Boolean);
begin

end;

procedure TIdeNotifier.BeforeCompile(const Project: IOTAProject;
  var Cancel: Boolean);
begin

end;

procedure TIdeNotifier.FileNotification(NotifyCode: TOTAFileNotification;
  const FileName: string; var Cancel: Boolean);
begin
  if NotifyCode = ofnProjectDesktopLoad then
    OTAMenuForm.SetUp
end;

initialization

finalization
  if Assigned(OTAMenuForm) then begin
    OTAMenuForm.Services.RemoveNotifier(OTAMenuForm.NotifierIndex);
    OTAMenuForm.Close;
    FreeAndNil(OTAMenuForm);
  end;
end.

我将其作为单独的答案发布,因为它使用了不同的实现 基于 ToolsAPI 的调试器可视化工具。可视化工具中有示例 Delphi 源代码的子文件夹。看起来最有前途的一个 起点是 StringListVisualizer.Pas 文件中的示例。然而,我发现 在最初的几次阅读中难以理解,结果证明它实际上并没有 做我想做的事。

下面的代码,当然需要编译成IDE包 需要 rtl 和 designide 单位,基于更简单的 DateTime 示例可视化工具,但适用于 TStrings 个对象的 Text 属性。这种改编仍然需要大量的工作,这也是我发布这个额外答案的主要原因,以免其他人头疼。

通常,TStrings 变量的 Text 属性 在 Watch Window 中显示为一个或多个文本行,用单引号括起来并用字符串#$D#$A。该代码删除了单引号并将#$D#$A 替换为space。这是在代码顶部附近的 GetReplacementValue 函数内完成的。其余代码只是实现可视化工具所需的包袱,即使在这个相当简约的实现中,代码也很多。

包安装完成后,也会显示在 Watch WindowText 属性 可以使用 Copy Watch Value 粘贴到剪贴板 Watch Window 的上下文菜单中的条目。

代码(为 XE4 编写并测试):

{*******************************************************}
{                                                       }
{            RadStudio Debugger Visualizer Sample       }
{ Copyright(c) 2009-2013 Embarcadero Technologies, Inc. }
{                                                       }
{*******************************************************}

{Adapted by Martyn Ayers, Bristol, UK Oct 2015}

unit SimpleTStringsVisualizeru;

interface

procedure Register;

implementation

uses
  Classes, Forms, SysUtils, ToolsAPI;

resourcestring
  sVisualizerName = 'TStrings Simple Visualizer for Delphi';
  sVisualizerDescription = 'Simplifies TStrings Text property format';

const
  CRLFReplacement = ' ';

type
  TDebuggerSimpleTStringsVisualizer = class(TInterfacedObject,
      IOTADebuggerVisualizer, IOTADebuggerVisualizerValueReplacer,
      IOTAThreadNotifier, IOTAThreadNotifier160)
  private
    FNotifierIndex: Integer;
    FCompleted: Boolean;
    FDeferredResult: string;
  public
    { IOTADebuggerVisualizer }
    function GetSupportedTypeCount: Integer;
    procedure GetSupportedType(Index: Integer; var TypeName: string;
      var AllDescendants: Boolean);
    function GetVisualizerIdentifier: string;
    function GetVisualizerName: string;
    function GetVisualizerDescription: string;
    { IOTADebuggerVisualizerValueReplacer }
    function GetReplacementValue(const Expression, TypeName, EvalResult: string): string;
    { IOTAThreadNotifier }
    procedure EvaluteComplete(const ExprStr: string; const ResultStr: string;
      CanModify: Boolean; ResultAddress: Cardinal; ResultSize: Cardinal;
      ReturnCode: Integer);
    procedure ModifyComplete(const ExprStr: string; const ResultStr: string;
      ReturnCode: Integer);
    procedure ThreadNotify(Reason: TOTANotifyReason);
    procedure AfterSave;
    procedure BeforeSave;
    procedure Destroyed;
    procedure Modified;
    { IOTAThreadNotifier160 }
    procedure EvaluateComplete(const ExprStr: string; const ResultStr: string;
      CanModify: Boolean; ResultAddress: TOTAAddress; ResultSize: LongWord;
      ReturnCode: Integer);
  end;


  TTypeLang = (tlDelphi, tlCpp);

//  The following function is the one which actually changes the TStrings
//  representation in the Watch Window
//
//  Normally, the Text property of TStrings variable is displayed in the Watch Window
//  and Evaluate window as one or more text lines surrounded by single quotes
//  and separated by the string #$D#$A
//
//  This implementation removes the single quotes and replaces the #$D#$A
//  by a space
//
//  Note the addition of '.Text' to the expression which gets evaluated; this is to
//  produce the desired result when using the 'Copy Watch Value' item in the
//  Watch Window context menu.

function TDebuggerSimpleTStringsVisualizer.GetReplacementValue(
  const Expression, TypeName, EvalResult: string): string;
var
  Lang: TTypeLang;
  i: Integer;
  CurProcess: IOTAProcess;
  CurThread: IOTAThread;
  ResultStr: array[0..4095] of Char; //  was 255
  CanModify: Boolean;
  ResultAddr, ResultSize, ResultVal: LongWord;
  EvalRes: TOTAEvaluateResult;
  DebugSvcs: IOTADebuggerServices;

  function FormatResult(const Input: string; out ResStr: string): Boolean;
  var
    TL : TStringList;
    i : Integer;
    S : String;
  const
    CRLFDisplayed = '#$D#$A';
  begin
    Result := True;
    ResStr := '';
    TL := TStringList.Create;

    try
      S := Input;
      S := StringReplace(S, CRLFDisplayed, #13#10, [rfReplaceAll]);
      TL.Text := S;
      for i := 0 to TL.Count - 1 do begin
        S := TL[i];
        if S <> '' then begin
          if S[1] = '''' then      //  Remove single quote at start of line
            Delete(S, 1, 1);
          if S[Length(S)] = '''' then  //  Remove single quote at end of line
            S := Copy(S, 1, Length(S) - 1);
        end;
        if ResStr <> '' then
          ResStr := ResStr + CRLFReplacement;
        ResStr := ResStr + S;
      end;
    finally
      TL.Free;
    end;
  end;

begin
  Lang := tlDelphi;
  if Lang = tlDelphi then
  begin
    if Supports(BorlandIDEServices, IOTADebuggerServices, DebugSvcs) then
      CurProcess := DebugSvcs.CurrentProcess;
    if CurProcess <> nil then
    begin
      CurThread := CurProcess.CurrentThread;
      if CurThread <> nil then
      begin
        EvalRes := CurThread.Evaluate(Expression + '.Text', @ResultStr, Length(ResultStr),
          CanModify, eseAll, '', ResultAddr, ResultSize, ResultVal, '', 0);
        if EvalRes = erOK then
        begin
          Result := ResultStr;
        end else if EvalRes = erDeferred then
        begin
          FCompleted := False;
          FDeferredResult := '';
          FNotifierIndex := CurThread.AddNotifier(Self);
          while not FCompleted do
            DebugSvcs.ProcessDebugEvents;
          CurThread.RemoveNotifier(FNotifierIndex);
          FNotifierIndex := -1;
          if (FDeferredResult = '') then
            Result := EvalResult
          else
            FormatResult(FDeferredResult, Result);
        end;
      end;
    end;
  end
  else
    ;
end;

procedure TDebuggerSimpleTStringsVisualizer.AfterSave;
begin
  // don't care about this notification
end;

procedure TDebuggerSimpleTStringsVisualizer.BeforeSave;
begin
  // don't care about this notification
end;

procedure TDebuggerSimpleTStringsVisualizer.Destroyed;
begin
  // don't care about this notification
end;

procedure TDebuggerSimpleTStringsVisualizer.Modified;
begin
  // don't care about this notification
end;

procedure TDebuggerSimpleTStringsVisualizer.ModifyComplete(const ExprStr,
  ResultStr: string; ReturnCode: Integer);
begin
  // don't care about this notification
end;

procedure TDebuggerSimpleTStringsVisualizer.EvaluteComplete(const ExprStr,
  ResultStr: string; CanModify: Boolean; ResultAddress, ResultSize: Cardinal;
  ReturnCode: Integer);
begin
  EvaluateComplete(ExprStr, ResultStr, CanModify, TOTAAddress(ResultAddress),
    LongWord(ResultSize), ReturnCode);
end;

procedure TDebuggerSimpleTStringsVisualizer.EvaluateComplete(const ExprStr,
  ResultStr: string; CanModify: Boolean; ResultAddress: TOTAAddress; ResultSize: LongWord;
  ReturnCode: Integer);
begin
  FCompleted := True;
  if ReturnCode = 0 then
    FDeferredResult := ResultStr;
end;

function TDebuggerSimpleTStringsVisualizer.GetSupportedTypeCount: Integer;
begin
  Result := 1;
end;

procedure TDebuggerSimpleTStringsVisualizer.GetSupportedType(Index: Integer; var TypeName: string;
  var AllDescendants: Boolean);
begin
  AllDescendants := True;
  TypeName := 'TStrings';
end;

function TDebuggerSimpleTStringsVisualizer.GetVisualizerDescription: string;
begin
  Result := sVisualizerDescription;
end;

function TDebuggerSimpleTStringsVisualizer.GetVisualizerIdentifier: string;
begin
  Result := ClassName;
end;

function TDebuggerSimpleTStringsVisualizer.GetVisualizerName: string;
begin
  Result := sVisualizerName;
end;

procedure TDebuggerSimpleTStringsVisualizer.ThreadNotify(Reason: TOTANotifyReason);
begin
  // don't care about this notification
end;

var
  TStringsVis: IOTADebuggerVisualizer;

procedure Register;
begin
  TStringsVis := TDebuggerSimpleTStringsVisualizer.Create;
  (BorlandIDEServices as IOTADebuggerServices).RegisterDebugVisualizer(TStringsVis);
end;

procedure RemoveVisualizer;
var
  DebuggerServices: IOTADebuggerServices;
begin
  if Supports(BorlandIDEServices, IOTADebuggerServices, DebuggerServices) then
  begin
    DebuggerServices.UnregisterDebugVisualizer(TStringsVis);
    TStringsVis := nil;
  end;
end;

initialization
finalization
  RemoveVisualizer;
end.