Extending the IDE/zh CN

From Free Pascal wiki
Jump to: navigation, search

Deutsch (de) English (en) español (es) français (fr) 日本語 (ja) русский (ru) slovenčina (sk) 中文(中国大陆)‎ (zh_CN)

扩展IDE

概述

IDE支持以下几种插件:

组件(Components) 
就是那些在控件面板上的东西。例如TButton可以用来创建按钮。
组件编辑器(Component Editors) 
当你在界面设计器(designer)上双击一个控件的时候,就会用到组件编辑器。扩展当右击一个控件时弹出的右键菜单里的条目的时候,也会用到组件编辑器。
属性编辑器(Property Editors) 
被对象监视器(object inspector)中的那些字段(row)使用。
砖家?(Experts) 
这是完全不同于其他的一种,他们注册新的菜单项和快捷方式,或者扩展其它的IDE特性。


有两种方式把你编写的插件添加到Lazarus里:

  1. 写一个包(package),安装它,并在你编写的单元中的Register函数中写代码注册之。
  2. 扩展lazarus的源代码,然后把svn diff发送到lazarus的邮件列表。

没有翻译的句子包含不会翻的和不感兴趣懒得翻的,以及还没翻完的。术语之类会在跟随的括号里给出英文。

菜单项

你可以添加新的菜单项,隐藏或移动已经存在的菜单项,也可以添加子菜单和sections。For your own forms you can register the TMainMenu/TPopupMenu so that other packages can extend your form further. 包IDEIntf中的单元MenuIntf包含了all registration for menus and many standard menu items of the IDE itself.

添加一个菜单项

诸如'View Object Inspector'之类的单独一个菜单项被称作一个TIDEMenuCommand。你可以使用RegisterIDEMenuCommand来创建一个这种菜单项,这个函数有两种形式(参数挺多的):

function RegisterIDEMenuCommand(Parent: TIDEMenuSection;
                                const Name, Caption: string;
                                const OnClickMethod: TNotifyEvent = nil;
                                const OnClickProc: TNotifyProcedure = nil;
                                const Command: TIDECommand = nil;
                                const ResourceName: String = ''
                                ): TIDEMenuCommand; overload;
function RegisterIDEMenuCommand(const Path, Name, Caption: string;
                                const OnClickMethod: TNotifyEvent = nil;
                                const OnClickProc: TNotifyProcedure = nil;
                                const Command: TIDECommand = nil;
                                const ResourceName: String = ''
                                ): TIDEMenuCommand; overload;

这两种形式的区别只有你指定父菜单(parent menu section,俺就是不懂section是个么意思)的方式。你可以通过路径直接或间接给定menu section。许多标准的sections可以在单元MenuIntf中被找到。例如section mnuTools,就是主IDE窗口上的工具(Tools)菜单。它包含一个名为itmSecondaryTools的section, 这个section是第三方工具被推荐使用的section。

下面的代码注册了一个新的菜单命令(menu command),名为MyTool,标题为Start my tool,但被点击会执行过程StartMyTool

procedure StartMyTool;
begin
  ...executed when menu item is clicked...
end;
 
procedure Register;
begin
  RegisterIDEMenuCommand(itmSecondaryTools, 'MyTool','Start my tool',nil,@StartMyTool);
end;

如果你想在点击后调用一个对象的方法而非这种过程(procedure),使用OnClickMethod参数。这个菜单项没有快捷键。如果你需要添加快捷键支持,你必须创建一个TIDECommand并传给Command参数。例如:

uses ... IDECommands, MenuIntf;
...
var
  Key: TIDEShortCut;
  Cat: TIDECommandCategory;
  CmdMyTool: TIDECommand;
begin
  // register IDE shortcut and menu item
  Key := IDEShortCut(VK_UNKNOWN,[],VK_UNKNOWN,[]);
  Cat:=IDECommandList.FindCategoryByName(CommandCategoryToolMenuName);
  CmdMyTool := RegisterIDECommand(Cat,'Start my tool', 'Starts my tool to do some stuff', Key, nil, @StartMyTool);
  RegisterIDEMenuCommand(itmSecondaryTools, 'MyTool', 'Start my tool', nil, nil, CmdMyTool);
end;

配置文件

加载和保存配置

所有IDE的配置文件都以xml的形式放在同一个目录下。包们可以把它们自己的配置文件也放在那里。那个目录可以通过如下形式获取:

uses LazIDEIntf;
...
  Directory:=LazarusIDE.GetPrimaryConfigPath;

包们可以通过如下形式创建它们自己的xml格式配置文件:

uses
  ..., LCLProc, BaseIDEIntf, LazConfigStorage;
 
const
  Version = 1;
var
  Config: TConfigStorage;
  SomeValue: String;
  SomeInteger: Integer;
  SomeBoolean: Boolean;
  FileVersion: LongInt;
begin
  SomeValue:='Default';
  SomeInteger:=3;
  SomeBoolean:=true;
 
  // 保存配置
  try
    Config:=GetIDEConfigStorage('mysettings.xml',false);
    try
      // 保存版本号,以便未来的扩展可以兼容处理它们
      Config.SetDeleteValue('Path/To/The/Version',Version,0);
      // 保存字符串字段"SomeValue"
      // 如果SomeValue的值(第二个参数)就是默认值(第三个参数),这个字段就不会被保存,
      // 因此只有与默认值不同的字段才会被保存
      // 这样一来xml可以尽量短小,默认值在未来换起来也方便
      Config.SetDeleteValue('Path/To/Some/Value',SomeValue,'Default');
      Config.SetDeleteValue('Path/To/Some/Integer',SomeInteger,3);
      Config.SetDeleteValue('Path/To/Some/Boolean',SomeBoolean,true);
      // SetDeleteValue还有其他的重载形式,按下Ctrl+Space自个儿琢磨吧(我勒个去这货不是输入法这货不是输入法……)
    finally
      Config.Free;
    end;
 
  except
    on E: Exception do begin
      DebugLn(['Saving mysettings.xml failed: ',E.Message]);
    end;
  end;
 
  // 加载配置
  try
    Config:=GetIDEConfigStorage('mysettings.xml',true);
    try
      // 读取配置的版本
      FileVersion:=Config.GetValue('Path/To/The/Version',0);
      // 读取字符串字段"SomeValue",如果不存在,使用默认值
      SomeValue:=Config.GetValue('Path/To/Some/Value','Default');
      SomeInteger:=Config.GetValue('Path/To/Some/Integer',3);
      SomeBoolean:=Config.GetValue('Path/To/Some/Boolean',true);
    finally
      Config.Free;
    end;
  except
    on E: Exception do begin
      DebugLn(['Loading mysettings.xml failed: ',E.Message]);
    end;
  end;
end;

组件

编写组件

你可以通过包编辑器(package editor)创建新组件。例如:要创建或打开一个包,点击add,再点New Component,填写条目:Ancestor Type = TButton,New class name = TMyButton,palette page = Misc,Unit file name = mybutton.pas (这个文件不会被创建的),单元名称设置为MyButton然后点击ok。 (想起小时候看的一本win95还是win98的书,那里面双击叫双咔嗒,单击叫单咔嗒。题外话,可乐加怀旧。)

给这个新组件添加个图标(显示在控件面板上)

For example give TMyButton an icon. Create an image file of the format .bmp, .xpm or .png with the same name as the component class. For example tmybutton.png and save it in the package source directory. The image can be created by any graphic program (e.g. gimp) and should be no bigger than 24x24 pixel. Then convert the image to a .lrs file with the lazres tool, which can be found in the lazarus/tools directory:

 ~/lazarus/tools/lazres mybutton.lrs tmybutton.png

This creates an pascal include file, which is used in the initialization section of mybutton.pas:

 initialization
   {$I mybutton.lrs}

Install the package.

在控件面板上隐藏一个组件

Package IDEIntf, unit componentreg:

IDEComponentPalette.FindComponent('TButton').Visible:=false;

编写组件编辑器(component editors)

组件编辑器应该用来处理双击界面编辑器中的一个控件,或者处理添加右键菜单项(在右击一个组件的时候)。编写一个组件编辑器是很简单的。 看看包IDEIntf中的单元componenteditors.pas,里面很多例子。例如双击组件时唤出代码编辑器,CheckListBox的实现是TCheckListBoxComponentEditor。

编写属性编辑器

在对象观察器(Object Inspector)中的每种类型(integer, string, TFont, ...)的属性都需要一个属性编辑器。如果你的组件包含一个名为Password的字符串类型的属性,你得相应地定义一个属性编辑器。 包IDEIntf中的单元propedits.pp包含很多标准的正被IDE使用的属性编辑器。例如,TStringPropertyEditor是所有字符串值的默认编辑器,但TComponentNamePropertyEditor就会更有针对性,它只处理TComponent.Name。

设计器(Designer)

编写一个设计器中间层(mediator,调解着,中介)

The standard designer allows to visually edit LCL controls, while all others are shown as icons. To visually edit non LCL control, you can create a designer mediator. This can be used to design webpages, UML diagrams or other widgetsets like fpGUI. There is a complete example in examples/designnonlcl/.

  • Install the example package examples/designnonlcl/notlcldesigner.lpk and restart the IDE. This will register the designer mediator for TMyWidget components and add the new components TMyButton, TMyGroupBox to the component palette.
  • Open the the example project examples/designnonlcl/project/NonLCL1.lpi.
  • Open the unit1.pas and show the designer form (F12). You should see the components as red rectangles, which can be selected, moved and resized like LCL controls.

如何在设计观察器和对象观察器中选择一个组件

uses propedits;
..
GlobalDesignHook.SelectOnlyThis(AComponent);

事件处理

IDE中有一些事件是可以被你编写的插件处理的。

界面设计器事件

在propedits.pp中有个"GlobalDesignHook"对象,它维护了几个和界面设计相关的事件。每个事件(event)会调用一个队列的事件处理(handler)。默认的事件处理是IDE添加的。你可以用AddHandlerXXX和RemoveHandlerXXX方法来添加你自己的事件处理。它们会在默认的事件处理被调用之前而被调用。

例子一枚:

 添加你的handler(通常在构造函数干这事儿):
   GlobalDesignHook.AddHandlerComponentAdded(@YourOnComponentAdded);
删除你的handler: GlobalDesignHook.RemoveHandlerComponentAdded(@YourOnComponentAdded);
你可以一下干掉所有的handlers。例如,把这行加到你的析构函数里应该是个好点子: GlobalDesignHook.RemoveAllHandlersForObject(Self);

GlobalDesignHook的handler们:

 // lookup root
 ChangeLookupRoot
   Called when the "LookupRoot" changed.
   The "LookupRoot" is the owner object of the currently selected components.
   Normally this is a TForm.
// methods CreateMethod GetMethodName GetMethods MethodExists RenameMethod ShowMethod Called MethodFromAncestor ChainCall
// components GetComponent GetComponentName GetComponentNames GetRootClassName ComponentRenamed Called when a component was renamed ComponentAdded Called when a new component was added to the LookupRoot ComponentDeleting Called before a component is freed. DeleteComponent Called by the IDE to delete a component. GetSelectedComponents Get the current selection of components.
// persistent objects GetObject GetObjectName GetObjectNames
// modifing Modified Revert RefreshPropertyValues // Selection SetSelection GetSelection

如何在一个设计窗体被修改后得到通知

每一个设计好的LCL窗体有一个TIDesigner类型的设计器。IDE创建的设计器都是TComponentEditorDesigner类型的,该类型定义在包IDEIntf中的单元componenteditors。 例如:

procedure TYourAddOn.OnDesignerModified(Sender: TObject);
var
  IDEDesigner: TComponentEditorDesigner;
begin
  IDEDesigner:=TComponentEditorDesigner(Sender);
  ...
end;
 
procedure TYourAddOn.ConnectDesignerForm(Form1: TCustomForm);
var
  IDEDesigner: TComponentEditorDesigner;
begin
  IDEDesigner:=TComponentEditorDesigner(Form1.Designer);
  IDEDesigner.AddHandlerModified(@OnDesignerModified);
end;

Project events

These events are defined in unit LazIDEIntf.

  • LazarusIDE.AddHandlerOnProjectClose: called before a project is closed
  • LazarusIDE.AddHandlerOnProjectOpened: called after the project was completely opened (for example all required packages were loaded, units were opened in the source editor)
  • LazarusIDE.AddHandlerOnSavingAll: called before IDE saves everything
  • LazarusIDE.AddHandlerOnSavedAll: called after IDE saved everything
  • LazarusIDE.AddHandlerOnProjectBuilding: called before IDE builds the project
  • LazarusIDE.AddHandlerOnProjectDependenciesCompiling: called before IDE compiles package dependencies of project
  • LazarusIDE.AddHandlerOnProjectDependenciesCompiled: called after IDE compiled package dependencies of project

Other IDE events

  • LazarusIDE.AddHandlerOnIDERestoreWindows: called when IDE is restores its windows (before opening the first project)
  • LazarusIDE.AddHandlerOnIDEClose: called when IDE is shutting down (after closequery, so no more interactivity)
  • LazarusIDE.AddHandlerOnQuickSyntaxCheck: called when the menu item or the shortcut for Quick syntax check is executed

Project

Current Project

The current main project can be obtained by LazarusIDE.ActiveProject. (unit LazIDEIntf)

All units of current project

To iterate through all pascal units of the current main project of the IDE you can use for example:

uses LCLProc, FileUtil, LazIDEIntf, ProjectIntf;
 
procedure ListProjectUnits;
var
  LazProject: TLazProject;
  i: Integer;
  LazFile: TLazProjectFile;
begin
  LazProject:=LazarusIDE.ActiveProject;
  if LazProject<>nil then
    for i:=0 to LazProject.FileCount-1 do
    begin
      LazFile:=LazProject.Files[i];
      if LazFile.IsPartOfProject
      and FilenameIsPascalUnit(LazFile.Filename)
      then
        debugln(LazFile.Filename);
    end;
end;

The .lpr, .lpi and .lps file of a project

uses LCLProc, FileUtil, ProjectIntf, LazIDEIntf;
var
  LazProject: TLazProject;
begin
  LazProject:=LazarusIDE.ActiveProject;
  // every project has a .lpi file:
  DebugLn(['Project'' lpi file: ',LazProject.ProjectInfoFile]);
 
  // if the project session information is stored in a separate .lps file:
  if LazProject.SessionStorage<>pssNone then
    DebugLn(['Project'' lps file: ',LazProject.ProjectSessionFile]);
 
  // If the project has a .lpr file it is the main source file:
  if (LazProject.MainFile<>nil)
  and (CompareFileExt(LazProject.MainFile.Filename,'lpr')=0) then
    DebugLn(['Project has lpr file: ',LazProject.MainFile.Filename]);
end;

The executable / target file name of a project

There is a macro $(TargetFile), which can be used in paths and external tools. You can query the macro in code:

uses MacroIntf;
 
function MyGetProjectTargetFile: string;
begin
  Result:='$(TargetFile)';
  if not IDEMacros.SubstituteMacros(Result) then
    raise Exception.Create('unable to retrieve target file of project');
end;

See here for more macros: IDE Macros in paths and filenames.

Add your own project type

You can add items to the 'New ...' dialog:


Add your own file type

You can add items to the 'New ...' dialog:

  • See the unit ProjectIntf of the package IDEIntf.
  • Choose a base class TFileDescPascalUnit for normal units or TFileDescPascalUnitWithResource for a new form/datamodule.

Add a new file type

uses ProjectIntf;
...
  { TFileDescText }
 
  TFileDescMyText = class(TProjectFileDescriptor)
  public
    constructor Create; override;
    function GetLocalizedName: string; override;
    function GetLocalizedDescription: string; override;
  end;
...
 
procedure Register;
 
implementation
 
procedure Register;
begin
  RegisterProjectFileDescriptor(TFileDescMyText.Create,FileDescGroupName);
end;
 
{ TFileDescMyText }
 
constructor TFileDescMyText.Create;
begin
  inherited Create;
  Name:='MyText'; // do not translate this
  DefaultFilename:='text.txt';
  AddToProject:=false;
end;
 
function TFileDescText.GetLocalizedName: string;
begin
  Result:='My Text'; // replace this with a resourcestring
end;
 
function TFileDescText.GetLocalizedDescription: string;
begin
  Result:='An empty text file';
end;

Add a new form type

uses ProjectIntf;
 
...
  TFileDescPascalUnitWithMyForm = class(TFileDescPascalUnitWithResource)
  public
    constructor Create; override;
    function GetInterfaceUsesSection: string; override;
    function GetLocalizedName: string; override;
    function GetLocalizedDescription: string; override;
  end;
...
 
procedure Register;
 
implementation
 
procedure Register;
begin
  RegisterProjectFileDescriptor(TFileDescPascalUnitWithMyForm.Create,FileDescGroupName);
end;
 
{ TFileDescPascalUnitWithMyForm }
 
constructor TFileDescPascalUnitWithMyForm.Create;
begin
  inherited Create;
  Name:='MyForm'; // do not translate this
  ResourceClass:=TMyForm;
  UseCreateFormStatements:=true;
end;
 
function TFileDescPascalUnitWithMyForm.GetInterfaceUsesSection: string;
begin
  Result:='Classes, SysUtils, MyWidgetSet';
end;
 
function TFileDescPascalUnitWithForm.GetLocalizedName: string;
begin
  Result:='MyForm'; // replace this with a resourcestring
end;
 
function TFileDescPascalUnitWithForm.GetLocalizedDescription: string;
begin
  Result:='Create a new MyForm from example package NotLCLDesigner';
end;

Packages

Search in all packages

Iterate all packages loaded in the IDE (since 0.9.29).

uses PackageIntf;
...
for i:=0 to PackageEditingInterface.GetPackageCount-1 do
  writeln(PackageEditingInterface.GetPackages(i).Name);

Search a package with a name

uses PackageIntf;
...
var
  Pkg: TIDEPackage;
begin
  Pkg:=PackageEditingInterface.FindPackageWithName('LCL');
  if Pkg<>nil then 
    ...
end;

Note: FindPackageWithName does not open the package editor. For that use DoOpenPackageWithName.

Windows

There are basically four types of IDE windows.

  • the main IDE bar is the Application.MainForm. It is always present.
  • floating/dockable windows like the Source Editors, the Object Inspectors and Messages.
  • the modal forms, like the find dialog, options dialogs and questions.
  • hints and completion forms

Adding a new dockable IDE window

What is a dockable IDE window: Windows like the Source Editor or the Object Inspector are floating windows, that can be docked if a docking package is installed, and its state, position and size is stored and restored on next IDE start. In order to restore a window the IDE needs a creator as defined in the unit IDEWindowIntf of the package IDEIntf. Each dockable window must have a unique name. Do not use generic names like 'FileBrowser' because this will easily clash with other packages. And don't use short names like 'XYZ', because the creator is responsible for all forms beginning with this name.

How to register a dockable IDE window

Remember to choose a long unique name that is a valid pascal identifier. Your window can have any caption you want.

uses SysUtils, IDEWindowIntf;
...
var MyIDEWindow: TMyIDEWindow = nil; 
 
procedure CreateMyIDEWindow(Sender: TObject; aFormName: string; var AForm: TCustomForm; DoDisableAutoSizing: boolean);
begin
  // check the name
  if CompareText(aFormName,MyIDEWindowName)<>0 then exit;
  // create the form if not already done and disable autosizing
  IDEWindowCreators.CreateForm(MyIDEWindow,TMyIDEWindowm,DoDisableAutosizing,Application);
  ... init the window ...
  AForm:=MyIDEWindow;
end;
 
procedure Register;
begin
  IDEWindowCreators.Add('MyIDEWindow',@CreateMyIDEWindow,nil,'100','50%','+300','+20%');
  // the default boundsrect of the form is:
  // Left=100, Top=50% of Screen.Height, Width=300, Height=20% of Screen.Height
  // when the IDE needs an instance of this window it calls the procedure CreateMyIDEWindow.
end;

Showing an IDE window

Do not use Show. Use:

IDEWindowCreators.ShowForm(MyIDEWindow,false);

This will work with docking. The docking system might wrap the form into a docking site. The BringToFront parameter tells the docking system to make the form and all its parent sites visible and bring the top level site to the front.

Notes about IDEWindowCreators and SimpleLayoutStorage

The IDEWindowCreators.SimpleLayoutStorage simply stores the BoundsRect and WindowState of all forms that were once opened. It is used as fallback if no dockmaster is installed. It stores the state even if a DockMaster is installed, so that when the dockmaster is uninstalled the forms bounds are restored.

The IDEWindowCreators is used by all dockable forms to register themselves and to show forms. When showing a form the Creator checks if a IDEDockMaster is installed and will delegate the showing to it. If no IDEDockMaster is installed it simply shows the form. The IDEDockMaster can use the information in the IDEWindowCreators to create forms by names and get an idea where to place a form when showing it for the first time. For more details see the packages AnchorDockingDsgn and EasyDockMgDsgn.

CodeTools

Before using the codetools you should commit the current changes of the source editor to the codetool buffers:

uses LazIDEIntf;
...
  // save changes in source editor to codetools
  LazarusIDE.SaveSourceEditorChangesToCodeCache(-1); // -1: commit all source editors

Adding a resource directive to a file

This adds a {$R example.res} to a pascal unit:

procedure AddResourceDirectiveToPascalSource(const Filename: string);
var
  ExpandedFilename: String;
  CodeBuf: TCodeBuffer;
begin
  // make sure the filename is trimmed and contains a full path
  ExpandedFilename:=CleanAndExpandFilename(Filename);
 
  // save changes in source editor to codetools
  LazarusIDE.SaveSourceEditorChangesToCodeCache(-1);
 
  // load the file
  CodeBuf:=CodeToolBoss.LoadFile(ExpandedFilename,true,false);
 
  // add the resource directive
  if not CodeToolBoss.AddResourceDirective(CodeBuf,'example.res') then
    LazarusIDE.DoJumpToCodeToolBossError;
end;

The codetools provides also functions like FindResourceDirective and RemoveDirective.

Help

Adding help for sources

First create a THelpDatabase:

 HelpDB:=TFPDocHTMLHelpDatabase(
    HelpDatabases.CreateHelpDatabase('ANameOfYourChoiceForTheDatabase',
                                            TFPDocHTMLHelpDatabase,true));
 HelpDB.DefaultBaseURL:='http://your.help.org/';
 FPDocNode:=THelpNode.CreateURL(HelpDB,
                  'Package1 - A new package',
                  'file://index.html');
 HelpDB.TOCNode:=THelpNode.Create(HelpDB,FPDocNode);// once as TOC
 DirectoryItem:=THelpDBISourceDirectory.Create(FPDocNode,'$(PkgDir)/lcl',
                                 '*.pp;*.pas',false);// and once as normal page
 HelpDB.RegisterItem(DirectoryItem);

Adding lines to the messages window

unit IDEMsgIntf;
...
var Dir: String;
begin
  Dir:=GetCurrentDir;
  IDEMessagesWindow.BeginBlock;
  IDEMessagesWindow.AddMsg('unit1.pas(30,4) Error: Identifier not found "a"',Dir,0);
  IDEMessagesWindow.EndBlock;
end;