Sign in Join
Wiki copytemporary

Revision 4 of 4, by WikiArchive on 11-10-2026. The page as it is now.

Multiplatform Programming Guide/zh CN

跨平台编程指南

本文是用 Lazarus 和 Free Pascal 编写跨平台应用程序的教程,涵盖了必要的注意事项,有助于创建可跨平台deploy的程序。

大多数 LCL 应用程序都可以跨平台,无需任何额外操作。 这一原则名为“一次编写、到处编译”。

跨平台编程简介

需要多平台运行吗?

要回答这个问题,首先应确定应用程序的潜在用户和使用方式。应用程序的部署环境决定了这个问题的答案。

如果是在 2014 年开发通用桌面软件,微软的 Windows 可能是最重要的平台。请注意,Mac OS X 和 Linux 正越来越受到欢迎,或许会成为应用程序的重要部署目标。

因国家/地区、软件类型、受众的不同,各种桌面操作系统的流行程度也各不相同,没有通用的规则。例如 macOS 在北美和西欧相当受欢迎,而在南美则大多只用于处理视频和音频。

在许多签约项目里,只与一个平台相关。Free Pascal 和 Lazarus 足以编写某个指定平台的软件,比如访问全部 Windows API 编写与 Windows 深度融合的程序。

若开发的是运行于 Web 服务器种的软件,则通常会采用一种 Unix 平台。这时可能 Linux、Solaris、*BSD 和其他 Unix 才是有效的部署目标,尽管出于完整性考虑可能会加入对 Windows 的支持。

只要解决了设计阶段的跨平台问题,就可以很大程度上忽略其他平台,就像为单个平台开发一样。不过在某些时候,程序还是需要在其他平台上测试、部署和运行。因此,最好是能自由访问目标操作系统。如果不想使用多台物理计算机,请研究一下双系统引导或虚拟机(VM),比如:

有些虚拟主机系统可轻松安装桌面 Linux 操作系统,只需鼠标单击一下即可,例如 Mac 系统中的 Parallels Desktop。

参阅 :小型虚拟主机系统 、 Qemu 和其他模拟器。

跨平台编程

文件和文件夹的处理

在处理文件和文件夹时,非常重要的一点就是采用平台无关的路径分隔符和行结束符。Lazarus 声明了以下常量,用于处理文件或文件夹。

另一个需注意的要点是文件系统是否大小写敏感。 Windows 平台的文件名通常不区分大小写,而 Unix 平台(如 Linux、FreeBSD)通常是区分的,不过承自 Unix 的 macOS 却不区分大小写。但请注意,如果在 Windows 中挂载 EXT2、EXT3 之类的文件系统,仍会区分大小写。同理,在 Linux 中挂载 FAT 文件系统也不应区分大小写。

NTFS 系统应特别引起注意,在 Windows 中使用时不区分大小写,但在 POSIX 操作系统中挂载时却又区分大小写。这可能会导致各种问题,包括文件丢失 ,如果 NTFS 分区中存在只是大小写不同的同名文件,挂载到 Windows 时就会丢失文件。开发人员应考虑用自定义函数进行检查,防止在 NTFS 上创建多个同名文件。

macOS 文件名默认不区分大小写。这可能会导致烦人的错误,因此任何可移植应用程序在使用文件名时都应该保持前后一致。

RTL 文件函数采用与文件名相同的操作系统字符编码。在 Windows 中就是某个 Windows 代码页,而在 Linux、BSD 和 macOS 中通常采用 UTF-8。LCL 的 FileUtil 单元提供了一些文件操作函数,与其他 LCL 代码一样使用 UTF-8 字符串。如下所示:

pascal
// 在 Linux、BSD、macOS 中,AnsiToUTF8 和 UTF8ToAnsi 需要 widestring manager 支持。
    // 不过这些操作系统通常采用 UTF-8 作为系统编码, 由此也无需用到 widestringmanager。
    function NeedRTLAnsi: boolean;// 如果系统编码不是 UTF-8 则返回 true
    procedure SetNeedRTLAnsi(NewValue: boolean);
    function UTF8ToSys(const s: string): string;     // 类似 UTF8ToAnsi 但对 widestringmanager 的依赖较少
    function SysToUTF8(const s: string): string;     // 类似 AnsiToUTF8 但对 widestringmanager 的依赖较少
    function UTF8ToConsole(const s: string): string; // 将 UTF8 字符串转换为控制台编码(Write、WriteLn 会用到)
    
    // 文件操作
    function FileExistsUTF8(const Filename: string): boolean;
    function FileAgeUTF8(const FileName: string): Longint;
    function DirectoryExistsUTF8(const Directory: string): Boolean;
    function ExpandFileNameUTF8(const FileName: string): string;
    function ExpandUNCFileNameUTF8(const FileName: string): string;
    function ExtractShortPathNameUTF8(Const FileName : String) : String;
    function FindFirstUTF8(const Path: string; Attr: Longint; out Rslt: TSearchRec): Longint;
    function FindNextUTF8(var Rslt: TSearchRec): Longint;
    procedure FindCloseUTF8(var F: TSearchrec);
    function FileSetDateUTF8(const FileName: String; Age: Longint): Longint;
    function FileGetAttrUTF8(const FileName: String): Longint;
    function FileSetAttrUTF8(const Filename: String; Attr: longint): Longint;
    function DeleteFileUTF8(const FileName: String): Boolean;
    function RenameFileUTF8(const OldName, NewName: String): Boolean;
    function FileSearchUTF8(const Name, DirList : String): String;
    function FileIsReadOnlyUTF8(const FileName: String): Boolean;
    function GetCurrentDirUTF8: String;
    function SetCurrentDirUTF8(const NewDir: String): Boolean;
    function CreateDirUTF8(const NewDir: String): Boolean;
    function RemoveDirUTF8(const Dir: String): Boolean;
    function ForceDirectoriesUTF8(const Dir: string): Boolean;
    
    // 运行环境
    function ParamStrUTF8(Param: Integer): string;
    function GetEnvironmentStringUTF8(Index: Integer): string;
    function GetEnvironmentVariableUTF8(const EnvVar: string): String;
    function GetAppConfigDirUTF8(Global: Boolean): string;
    
    // 其他
    function SysErrorMessageUTF8(ErrorCode: Integer): String;

空文件名和路径双分隔符

在处理文件/目录名时,Windows 与 Linux/Unix/类 Unix 系统有不同的做法。

对于文件名中的双路径分隔符,处理方式也不相同:

在拼接文件名时,这一点就很重要了。例如:

pascal
FullFilename:=FilePath+PathDelim+ShortFilename; // 可能会生成两个 PathDelims,在 Windows 和 Linux 中会
    给出��结果
    FullFilename:=AppendPathDelim(FilePath) + ShortFilename; // 只会生成一个 PathDelim
    FullFilename:=TrimFilename(FilePath+PathDelim+ShortFilename); // 只会生成一个 PathDelim,会再作些清理

TrimFilename 函数会将双路径分隔符替换为单个,并缩短 '..' 路径,比如会把 /usr//lib/../src 修整为 /usr/src 。

若要判断某个目录是否存在,请使用 DirectoryExistsUTF8 。

另一常见任务是检查文件名的 path 部分是否存在。ExtractFilePath 可用于获取路径,但会包含路径分隔符。

类 Unix 系统的根目录是 '/',用 ChompPathDelim 函数会创建一个空字符串。DirPathExists 函数的工作方式类似于 DirectoryExistsUTF8,只是会对给出的路径作裁剪。

请注意,Unix/Linux 使用 '~'(波浪号)表示 home 目录,通常用 '/home/jim/'表示名为 jim 的用户。所以在命令行和脚本代码中,'~/myapp/myfile' 和 '/home/jim/myapp/myfile' 是同一个意思。但 Lazarus 不会自动扩展波浪号。因此必须用 ExpandFileNameUTF8('~/myapp/myfile') 获取完整路径。

文本编码

文本文件通常采用当前系统的编码。在 Windows 中通常是某个代码页,而在 Linux、BSD 和 macOS 中通常采用 UTF-8。没有100%准确的规则能够找出文本文件的编码。LCL 的 lconvencoding 单元中有一个猜测文本编码的函数:

pascal
function GuessEncoding(const s: string): string;
    function GetDefaultTextEncoding: string;

其中还包含了一些编码转换函数:

pascal
function ConvertEncoding(const s, FromEncoding, ToEncoding: string): string;
    
    function UTF8BOMToUTF8(const s: string): string; // 带 BOM 的 UTF8
    function ISO_8859_1ToUTF8(const s: string): string; // 中欧
    function CP1250ToUTF8(const s: string): string; // 中欧
    function CP1251ToUTF8(const s: string): string; // 西里尔
    function CP1252ToUTF8(const s: string): string; // 拉丁语1
    ...
    function UTF8ToUTF8BOM(const s: string): string; // 带 BOM 的 UTF8
    function UTF8ToISO_8859_1(const s: string): string; // 中欧
    function UTF8ToCP1250(const s: string): string; // 中欧
    function UTF8ToCP1251(const s: string): string; // 西里尔
    function UTF8ToCP1252(const s: string): string; // 拉丁语1
    ...

比如要读入文本文件并转换为 UTF-8 编码,可用如下代码:

pascal
var
      sl: TStringList;
      OriginalText: String;
      TextAsUTF8: String;
    begin
      sl:=TStringList.Create;
      try
        sl.LoadFromFile('sometext.txt'); // 注意:这会把文件的行结束符修改为系统当前的行结束符
        OriginalText:=sl.Text;
        TextAsUTF8:=ConvertEncoding(OriginalText,GuessEncoding(OriginalText),EncodingUTF8);
        ...
      finally
        sl.Free;
      end;
    end;

若要以系统编码保存文本文件,可用以下代码:

pascal
sl.Text:=ConvertEncoding(TextAsUTF8,EncodingUTF8,GetDefaultTextEncoding);
    sl.SaveToFile('sometext.txt');

配置文件

SysUtils 单元的 GetAppConfigDir 函数可获取各平台中的配置文件存放位置。该函数有一个 Global 参数,为 true 则返回全局目录,即适用于系统中的所有用户。如果参数 Global 为 false,则配置文件目录仅对运行该程序的用户有效。在不支持多用户的系统中,上述两个目录可能相同。

还有一个 GetAppConfigFile 函数,将返回一个配置文件名,用法如下:

pascal
ConfigFilePath := GetAppConfigFile(False);

下面是在不同系统中用默认路径输出的示例:

pascal
program project1;
    
    {$mode objfpc}{$H+}
    
    uses
      SysUtils;
    
    begin
      WriteLn(GetAppConfigDir(True));
      WriteLn(GetAppConfigDir(False));
      WriteLn(GetAppConfigFile(True));
      WriteLn(GetAppConfigFile(False));
    end.

可见全局配置文件存于 /etc 目录中,而当前配置存于用户 home 目录中的隐藏目录。在 UNIX 和类 UNIX 系统中,目录名称以句点(.)开头的是隐藏目录。可在 GetAppConfigDir 返回的目录中创建一个目录,然后将配置文件存于其中。

https://upload.wikimedia.org/wikipedia/commons/d/d8/Nuvola_apps_ktip.png Note: 普通用户对 /etc 目录没有写入权限。只有具备管理员权限的用户才行。

请注意,在 FPC 2.2.4 以下版本中,GetAppConfigDir 函数用程序所在的目录存储 Windows 全局配置。

Windows 98 下 FPC 2.2.0 的输出:

pascal
C:\Program Files\PROJECT1
    C:\Windows\Local Settings\Application Data\PROJECT1
    C:\Program Files\PROJECT1\PROJECT1.cfg
    C:\Windows\Local Settings\Application Data\PROJECT1\PROJECT1.cfg

Windows XP 下 FPC 3.0.4 的输出:

pascal
C:\Documents and Settings\All Users\Application Data\project1\
    C:\Documents and Settings\user\Local Settings\Application Data\project1\
    C:\Documents and Settings\All Users\Application Data\project1\project1.cfg
    C:\Documents and Settings\user\Local Settings\Application Data\project1\project1.cfg

Windows 7 和 Windows 10 下 FPC 3.0.4 的输出:

pascal
C:\ProgramData\project1\
    C:\Users\user\AppData\Local\project1\
    C:\ProgramData\project1\project1.cfg
    C:\Users\user\AppData\Local\project1\project1.cfg

macOS 10.14.5 下 FPC 3.0.4 的输出(不符合 Apple 规则 - 正确的 macOS 配置文件位置请参阅below):

pascal
/etc/project1/
    /Users/user/.config/project1/
    /etc/project1.cfg
    /Users/user/.config/project1.cfg

FreeBSD 12.1 下 FPC 3.0.4 的输出:

pascal
/etc/project1/
    /home/user/.config/project1/
    /etc/project1.cfg
    /home/user/.config/project1.cfg

请注意 Windows 和非 Windows 系统的输出差异,Windows 系统中

pascal
WriteLn(GetAppConfigFile(True));

输出的全局配置包含一个子目录,而其他操作系统则没有。若要获得与非 Windows 系统相同的结果,需要多加一个 Boolean参数:

pascal
WriteLn(GetAppConfigFile(True,True));。

https://upload.wikimedia.org/wikipedia/commons/d/d8/Nuvola_apps_ktip.png Note: UPX 会影响 GetAppConfigDir 和 GetAppConfigFile 函数的使用效果。

macOS

配置文件大多数情况下就是首选项文件,在 macOS 中则应为“.plist”扩展名结尾的 XML 文件,存于 /Library/Preferences 或 ~/Library/Preferences 中,文件名则取自程序包 Info.plist 文件中的“Bundle identifier”字段。 采用用户目录中的 .config 文件违反了 Apple 编程规则。 请参阅定位 macOS 应用程序的支持库、首选项文件夹一文,了解以 Apple 方式处理配置文件的代码。

数据和资源文件

应用程序需用到的数据文件应该存在何处,比如图像、音乐、XML 文件、数据库文件、帮助文件等,这是个十分常见的问题。不幸的是,获取数据文件最优位置的跨平台函数根本不存在。解决方案就是用 IFDEF 为各种平台实现不同的代码。

Windows

在 Windows 中,由程序修改的数据不应存于应用程序目录中(如 C:\Program Files\),而应放在特定位置(可参阅"对应用程序数据进行分类"( 链接已失效 )。Windows Vista 及以上版本会强制如此(用户只能在提权或禁用 UAC 时,才对这些目录具有写入权限),不过也会用文件夹重定向机制来兼容老程序或编码错误程序。对应用程序目录中的数据可以只读(不写),但不推荐。

简单的用法可以如下所示:

pascal
OpDirLocal:= GetEnvironmentVariableUTF8('appdata')+'\MyAppName';

请参阅Windows 编程技巧 - 获取特殊文件夹。

Unix/Linux

在大多数 Unix 系统中(如 Linux、FreeBSD、OpenBSD、Solaris,但 macOS 除外 ),应用程序的数据文件存于固定的位置,可能是 /usr/share/app_name 或 /opt/app_name。

由应用程序写入的数据,通常存于 /usr/local/var/app_name、/usr/local/share/app_name 或 /usr/local/app_name,访问权限会自动设好。

帮助文件(又名 man)应存于 /usr/local/man/man[手册章节号]/app_name>,访问权限会自动设好。请注意,某些 FreeBSD 和 Linux 手册的章节位置会有不同,或者干脆缺失。

由用户读写的配置文件/数据,将存于用户 home 目录中的某处(比如 ~/.config/<programname>)。请参阅上文配置文件。

macOS

macOS 是 UNIX 操作系统的一个例外。应用程序以 bundle 的形式发布(扩展名为“.app”的目录),文件管理器 Finder 将其视作单个文件(在 Terminal 中可执行“cd path/myapp.app”,或在 Finder 中右键单击应用程序并选择“显示包内容”)。应用程序的资源文件应存于 bundle 中。假定 bundle 为“path/MyApp.app”,则:

应用程序的数据和资源文件通常应存于应用程序 bundle 的 Resources 目录中。有关如何以 Apple 兼容方式处理应用程序数据的代码,请参阅文章 定位 macOS 应用程序资源目录 。

用户生成的数据文件应于用户的“~/Library/Application Support”目录中。有关如何以 Apple 兼容方式处理用户数据的代码,请参阅文章定位 macOS 应用程序的支持库、首选项文件夹。

{{Warning|请勿 在任何 UNIX 平台中用 paramStr(0)

pascal
或任何用到它的函数æ�¥ _确定_ å�¯æ‰§è¡Œæ–‡ä»çš„ä½�置,因为这是 DOS-Windows-OS/2 系统的约定,在概念上存在一些问题,且在å…ä»–å¹³å�°ä¸Šæ— æ³•模拟解决。在 UNIX å¹³å�°ä¸­ï¼Œ

paramStr(0)

pascal
唯一能ä¿�è¯�返回的是程åº�å�¯åЍæ—çš„å��称。

程序所在的目录和二进制文件的真实名称(若以符号链接启动),用 paramStr(0) 均无法可靠获得。对于 macOS 而言,可用此文中的原生代码可靠地 得到应用程序 bundle 目录。

32/64 bit

在运行时检测系统位数

虽然用编译指令 define 可以控制 32 位或 64 位编译,但有时会想知道操作系统的位数情况。 比如在 64 位 Windows 中运行 32 位 Lazarus 程序,可能需要运行一个位于 32 位程序的文件目录中的外部程序,或者可能需向用户显示信息:在 LazUpdater Lazarus 安装程序中,需要让用户选择 32 位和 64 位编译器。这里是代码: 检测 Windows x32-x64 示例。

在加载外部库之前检测位数

若要从动态库加载函数,则动态库必须与应用程序位数相同。在 64 位 Windows 中,应用程序可能是 32 位或 64 位,并且系统中可能同时存在 32 位和 64 位的动态库。因此,在动态加载 dll 之前,可能需要检测 dll 的位数是否与应用程序相同。以下是检测 dll 位数的函数:(由 GetMem 贡献于论坛)

pascal
uses {..., } JwaWindows;
    
    function GetPEType(const APath: WideString): Byte;
    const
      PE_UNKNOWN = 0; //if the file is not a valid dll, 0 is returned
     // PE_16BIT   = 1; // not supported by this function
      PE_32BIT   = 2;
      PE_64BIT   = 3;
    var
      hFile, hFileMap: THandle;
      PMapView: Pointer;
      PIDH: PImageDosHeader;
      PINTH: PImageNtHeaders;
      Base: Pointer;
    begin
      Result := PE_UNKNOWN;
     
      hFile := CreateFileW(PWideChar(APath), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
      if hFile = INVALID_HANDLE_VALUE then
      begin
        CloseHandle(hFile);
        Exit;
      end;
     
      hFileMap  := CreateFileMapping(hFile, nil, PAGE_READONLY, 0, 0, nil);
      if hFileMap = 0 then
      begin
        CloseHandle(hFile);
        CloseHandle(hFileMap);
        Exit;
      end;
     
      PMapView := MapViewOfFile(hFileMap, FILE_MAP_READ, 0, 0, 0);
      if PMapView = nil then
      begin
        CloseHandle(hFile);
        CloseHandle(hFileMap);
        Exit;
      end;
     
      PIDH := PImageDosHeader(PMapView);
      if PIDH^.e_magic <> IMAGE_DOS_SIGNATURE then
      begin
        CloseHandle(hFile);
        CloseHandle(hFileMap);
        UnmapViewOfFile(PMapView);
        Exit;
      end;
     
      Base := PIDH;
      PINTH := PIMAGENTHEADERS(Base + LongWord(PIDH^.e_lfanew));
      if PINTH^.Signature = IMAGE_NT_SIGNATURE then
      begin
        case PINTH^.OptionalHeader.Magic of
          $10b: Result := PE_32BIT;
          $20b: Result := PE_64BIT
        end;
      end;
     
      CloseHandle(hFile);
      CloseHandle(hFileMap);
      UnmapViewOfFile(PMapView);
    end;
    
    //Now, if you compile your application for 32-bit and 64-bit windows, you can check if dll's bitness is same as your application's:
    function IsCorrectBitness(const APath: WideString): Boolean;
    begin  
      {$ifdef CPU32}
        Result := GetPEType(APath) = 2; //the application is compiled as 32-bit, we ask if GetPeType returns 2
      {$endif}
      {$ifdef CPU64}
        Result := GetPEType(APath) = 3; //the application is compiled as 64-bit, we ask if GetPeType returns 3
      {$endif}
    end;

指针/整数 类型转换

64 位指针要占用 8 个字节,而不是 32 位指针的 4 字节。而为了保持兼容性,Integer 类型在所有平台都保持 32 位。这就意味着,指针类型无法转换为整数,反之亦然。

为此 FPC 定义了两个类型:PtrInt 和 PtrUInt。PtrInt 在 32 位平台上是 32 位有符号整数,在 64 位平台上是 64 位有符号整数。PtrUInt 同理,只是换成无符号整数。

若要对 Delphi 和 FPC 都能有效,请使用以下代码:

pascal
{$IFNDEF FPC}
     type
       PtrInt = integer;
       PtrUInt = cardinal;
     {$ENDIF}

请将所有 integer(SomePointerOrObject) 都替换为 PtrInt(SomePointerOrObject) 。

字节序

Intel 平台是低位优先字节序,这意味着低位字节在前(低地址)。如 $1234 的两个字节会存为 $34 $12。而在 powerpc 这种高位优先的系统中,$1234 的两个字节会存为 $12 $34。在读取其他系统创建的文件时,这是一个重要区别。

若要适应两种字节序,请使用以下代码:

pascal
{$IFDEF ENDIAN_BIG}
    ...
    {$ELSE}
    ...
    {$ENDIF}

而低位优先字节序的编译符则为 ENDIAN_LITTLE。

system 单元中提供了大量的字节序转换函数,如 SwapEndian、BEtoN(高位优先到当前字节序)、LEtoN(低位优先到当前字节序)、NtoBE(当前字节序到高位优先)和 NtoLE(当前字节序到低位优先)。

Libc 和其他特殊单元

请避免使用“oldlinux” 和 “libc” 之类在 linux/i386 之外不受支持的过时单元。

Assembler

请避免使用 汇编语言。

编译器 define 指令

pascal
{$ifdef CPU32}
    ...编写�行于 32 �处�器下的代�
    {$ENDIF}
    {$ifdef CPU64}
    ...编写�行于 64 �处�器下的代�
    {$ENDIF}

项目、包和搜索路径

Lazarus 项目和软件包已设计为可跨平台编译。通常只要将项目和所需软件包复制到别的机器上,即可进行编译了。无需为每个平台分别创建项目。

下面给出一些跨平台编译的建议。

编译器会为每个单元创建同名 ppu 文件,以供其他项目和软件包使用。单元的源码文件(如 unit1.pas)不应在多个项目和软件包之间共用。只要向编译器给出创建 ppu 文件的单元输出目录即可。IDE 默认会执行此操作,因此无需执行任何操作。

每个单元文件都必须属于 某个 项目或软件包。如果单元文件只有一个项目用到了,请将其加入项目中。否则,请将其加入某个软件包。如果尚未为单元建立软件包,请参阅:为公共单元创建软件包。

每个项目和软件包都应位于独立 的目录 - 相互之间不应共用目录。不然,您就得吃透编译器的搜索路径。如果您不够熟练,或者使用项目/软件包的人不够熟练,请不要在项目/软件包之间共享目录。

平台依赖的单元

如单元 wintricks.pas 只能在 Windows 下使用。请在 uses 部分采用以下方式声明:

pascal
uses
      Classes, SysUtils
      {$IFDEF Windows}
      ,WinTricks
      {$ENDIF}
      ;

如果单元属于某个软件包,则还得在包编辑器中选中单元并不要勾选 Use unit 。

请参阅平台依赖的单元

平台依赖的搜索路径

若跨平台应用需直接访问操作系统,没完没了的 IFDEF 结构很快会让人厌倦不堪。FPC 和 Lazarus 源代码常用的一种解决方案,就是使用包含文件。先为每种平台都创建一个子目录,如 win32、linux、bsd、darwin。再在每个目录中放入文件名相同的包含文件。然后在 include 路径中使用宏。单元代码中则可以使用常规的 include 指令。

以下是某 LCL 部件的 include 示例:

为每种 LCL 部件创建 include 文件:

pascal
win32/example.inc
    gtk/example.inc
    gtk2/example.inc
    carbon/example.inc

这些 include 文件不用加入软件包或项目中。 在软件包或项目的编译器选项中,加入包含文件搜索路径 $(LCLWidgetType) 。

在单元代码中采用如下指令即可:{$I example.inc}

以下给出一些常用的宏及常见值:

$Env() 可用于引用环境变量。

当然可以组合使用这些宏。比如 LCL 就用到了:

pascal
$(LazarusDir)/lcl/units/$(TargetCPU)-$(TargetOS);$(LazarusDir)/lcl/units/$(TargetCPU)-$(TargetOS)/$(LCLWidgetType)

完整的宏清单请参阅: IDE 中的路径和文件名宏

依赖于计算机/用户的搜索路径

假设现有两台 Windows 计算机 stan 和 oliver。stan 中的单元文件位于 C:\units ,而 oliver 中的单位文件位于 D:\path 。这些单元文件都属于软件包 SharedStuff,在 stan 中是 C:\units\sharedstuff.lpk ,而在 oliver 中则是 D:\path\sharedstuff.lpk 。 只要 IDE 或 lazbuild 打开过 lpk 之后,其路径就会自动存于配置文件(packagefiles.xml) 中。当编译用到 SharedStuff 包的项目时,IDE 和 lazbuild 就能找到这个包。这时就无需什么配置文件。

若要在多台机器或为一台机器中的多个用户(如多个学生)部署软件包,那么可在 lazarus 源代码目录中加入一个 lpl 文件。相关示例请参阅 packager/globallinks。

地区差异

在 Free Pascal 中,有些函数会根据当前地区的不同产生不同的结果,例如 StrToFloat。比如在美国,小数分隔符通常是“.”,但在很多欧洲和南美国家/地区则是“,”。这可能会是个问题,因为有时希望这些函数能以固定的方式运行,不要依赖于地区。例如,包含小数点的文件格式就应始终以同一方式进行解析。

下面几节内容给出了具体做法。

macOS

关于 macOS 系统中地区设置的细节,请参阅文章 Locale settings for macOS。

StrToFloat

以下代码可创建将小数分隔符固定的格式设置:

pascal
// in your .lpr project file
    uses
    ...
    {$IFDEF UNIX}
    clocale 
    { required on Linux/Unix for formatsettings support. Should be one of the first (probably after cthreads?}
    {$ENDIF}

和:

pascal
// in your code:
    var
      FPointSeparator, FCommaSeparator: TFormatSettings;
    begin
      // Format settings to convert a string to a float
      FPointSeparator := DefaultFormatSettings;
      FPointSeparator.DecimalSeparator := '.';
      FPointSeparator.ThousandSeparator := '#';// disable the thousand separator
      FCommaSeparator := DefaultFormatSettings;
      FCommaSeparator.DecimalSeparator := ',';
      FCommaSeparator.ThousandSeparator := '#';// disable the thousand separator

后续在调用 StrToFloat 时即可使用上述格式设置,如下所示:

pascal
// This function works like StrToFloat, but simply tries two possible decimal separator
    // This will avoid an exception when the string format doesn't match the locale
    function AnSemantico.StringToFloat(AStr: string): Double;
    begin
      if Pos('.', AStr) > 0 then Result := StrToFloat(AStr, FPointSeparator)
      else Result := StrToFloat(AStr, FCommaSeparator);
    end;

GTK2 和 FPU 异常的屏蔽

Gtk2 库更改了 FPU (浮点单元) 对异常的默认屏蔽。因此用到 Gtk2 库的应用程序会无法触发一些浮点数类的异常。假如在 Windows 中开发采用了 win32/64 控件(Windows 默认)的 LCL 应用程序,并打算在 Linux 下也要能编译(默认控件是 Gtk2),就应牢记这些不兼容的地方。

在 此论坛主题和缺陷报告中有所答复,但显然这是无解的,因此我们必须清楚真正的差别是什么。

那就做个测试吧:

pascal
uses
      ..., math,...
    
    {...}
    
    var
      FPUException: TFPUException;
      FPUExceptionMask: TFPUExceptionMask;
    begin
      FPUExceptionMask := GetExceptionMask;
      for FPUException := Low(TFPUException) to High(TFPUException) do begin
        write(FPUException, ' - ');
        if not (FPUException in FPUExceptionMask) then
          write('not ');
    
        writeln('masked!');
      end;
      readln;
    end.

这个简单的程序会返回 FPC 默认的屏蔽设置:

pascal
exInvalidOp - not masked!
    exDenormalized - masked!
    exZeroDivide - not masked!
    exOverflow - not masked!
    exUnderflow - masked!
    exPrecision - masked!

但使用 Gtk2 时只有 exOverflow 未被屏蔽。

因此,如果应用程序链接了 Gtk2 库,就不会触发 EInvalidOp 和 EZeroDivide 异常!通常把非零值除以零就会引发 EZeroDivide 异常,让零除以零则会引发 EInvalidOp 异常。比如以下代码:

pascal
var
      X, A, B: Double;
    // ...
    
    try
      X := A / B;
      // 代码块 1
    except   
      // 代码块 2
    end;
    // ...

在采用 Gtk2 控件进行编译时,程序的走向将会不一样。采用 Windows 控件时,若 B 等于零会触发异常(EZeroDivide 或 EInvalidOp,取决于 A 是否为零),于是会执行“代码块 2”。在用 Gtk2 时,X 变为 Infinity、NegInfinity 或 NaN,并且会执行“代码块 1”。

这种不一致的情况可用各种方法克服。大多数情况下可以先检测一下 B,等于零就不再继续作除法了。但有时还需要采用一些其他方法。请看以下示例:

pascal
uses
      ..., math,...
    
    //...
    var
      X, A, B: Double;
      Ind: Boolean;
    // ...
    try
      X := A / B;
      Ind := IsInfinite(X) or IsNan(X); // 用 gtk2 时会执行至此
    except   
      Ind := True; // 用 windows 控件,B 为零时会执行至此
    end;
    if Ind then begin
      // 代码块 2
    end else begin
      // 代码块 1
    end;
    // ...

或者可以:

pascal
uses
      ..., math,...
    
    //...
    var
      X, A, B: Double;
      FPUExceptionMask: TFPUExceptionMask;
    // ...
    
    try
      FPUExceptionMask := GetExceptionMask;
      SetExceptionMask(FPUExceptionMask - [exInvalidOp, exZeroDivide]); // 去除屏蔽
      try
        X := A / B;
      finally
        SetExceptionMask(FPUExceptionMask); // 立即返回之前的屏蔽设置,不允许在异常屏蔽未设置的情况下调用 Gtk2 内部函数
      end;
      // 代码块 1
    except   
      // 代码块 2
    end;
    // ...

请小心,不要做类似以下的操作(移除异常屏蔽的情况下调用 LCL):

pascal
try
      FPUExceptionMask := GetExceptionMask;
      SetExceptionMask(FPUExceptionMask - [exInvalidOp, exZeroDivide]);
      try
        Edit1.Text := FloatToStr(A / B); // 不行!设置 Edit 的文本会深入调用控件的内部代码,不能在异常屏蔽没有设置的情况下调用 Gtk2 API!
      finally
        SetExceptionMask(FPUExceptionMask);
      end;
      // 代码块 1
    except   
      // 代码块 2
    end;
    // ...

而应使用辅助变量:

pascal
try
      FPUExceptionMask := GetExceptionMask;
      SetExceptionMask(FPUExceptionMask - [exInvalidOp, exZeroDivide]);
      try
        X := A / B; // First, we set auxiliary variable X
      finally
        SetExceptionMask(FPUExceptionMask);
      end;
      Edit1.Text := FloatToStr(X); // Now we can set Edit's text.
      // code block 1
    except   
      // code block 2
    end;
    // ...

在开发 LCL 应用程序时,任何时候最要紧的就是要清楚上述差异,牢记不同的控件可能会以不同方式执行某些浮点运算。当然可以考虑采用某种合适的方案来解决,但这个问题不应被忽视。

从 Windows 迁移到 *nix 等系统时的问题

本节讨论了与 Linux、macOS、Android 和其他 Unix 相关的一些主题。不一定适用于所有平台。

Unix 中没有“应用程序目录”的概念

许多程序员习惯调用 ExtractFilePath(ParamStr(0)) 或 Application.ExeName 来获取可执行文件的位置,然后根据可执行文件的位置搜索程序执行所需的文件(图像、XML 文件、数据库文件等)。这在 unix 系统中是不行的。ParamStr(0) 返回的可能不是程序执行时所在的目录,不同的 shell (sh、bash 等)还会返回不同的结果。

即便 Application.ExeName 实际反映了可执行文件所在的目录,这个可执行文件也可能只是个符号链接,因此得到的可能只是符号链接所在的目录(取决于 Linux 内核的版本,可能是符号链接的目录或程序二进制文件的目录)。

若要避免上述问题,请参阅配置文件和 数据文件。

平替 Windows COM 自动化

Windows 中的 COM 自动化是个强大的功能,不仅可以远程操控其他程序,还允许被其他程序操控。在 Delphi 中可以让应用程序既是 COM 自动化客户端又是服务端,这意味着可以与其他程序实现双向操控。 请参阅使用 COM 自动化与 OpenOffice 和 Microsoft Office 交互。示例请参阅利用 COM 自动化实现与 OpenOffice 和 Microsoft Office 的交互。

macOS 中的替代方案

遗憾的是,macOS 和 Linux 不支持 COM 自动化。不过可用 AppleScript 在 macOS 中模拟 COM 自动化的某些功能。

AppleScript 在某些方面类似于 COM 自动化。比如可以编写操纵其他程序的脚本。下面是一个非常简单的 AppleScript 示例,启动 NeoOffice(OpenOffice.org 的 Mac 版本):

pascal
tell application "NeoOffice"
       launch
     end tell

应用程序若支持 AppleScript 操纵,就提供了可供调用的类和命令“字典”,类似于 Windows 自动化服务程序的类。即便是 NeoOffice 这类不提供字典的应用程序,仍然会响应命令 “launch”、“activate” 和 “quit”。AppleScript 可由 macOS 脚本编辑器或 Finder 运行 ,甚至还能转换为与普通 App 一样可拖放至 Dock 的 App。从程序中也可以运行 AppleScript,如下例所示:

pascal
Shell('myscript.applescript');

这里假定脚本位于给出的文件中。还可以在应用程序中用 macOS OsaScript 命令动态执行脚本:

pascal
Shell('osascript -e '#39'tell application "NeoOffice"'#39 +
           ' -e '#39'launch'#39' -e '#39'end tell'#39);
           {注意用 #39 为参数裹上引号}

当然上述例子相当于以下 Open 命令:

pascal
Shell('open -a NeoOffice');

同理,在 macOS 中可以模拟 Windows shell 命令,以启动 Web 浏览器和电子邮件客户端:

pascal
fpsystem('open -a safari "<http://gigaset.com/shc/0,1935,hq_en_0_141387_rArNrNrNrN,00.html>"');

和

pascal
fpsystem('open -a mail "[mailto:ss4200@invalid.org](<mailto:ss4200@invalid.org>)"');

这里想当然假设 macOS 系统安装了 Safari 和 Mail 应用。当然,永远不应做出这种假设,其实上述两个示例可以靠 macOS 来完成,可以换成以下方式来调用当前用户的默认 Web 浏览器和电子邮件客户端:

pascal
fpsystem('open "<http://gigaset.com/shc/0,1935,hq_en_0_141387_rArNrNrNrN,00.html>"');

和

pascal
fpsystem('open "[mailto:ss4200@invalid.org](<mailto:ss4200@invalid.org>)"');

如果用了 fpsystem 或 shell(可互替),请不要忘记在 uses 中包含 Unix 单元。

AppleScript 的真正强大之处在于,能够远程操纵程序创建和打开文档,并自动执行其他一些功能。到底能对程序执行多少操纵,取决于其 AppleScript 词典(若存在的话)。比如用 AppleScript 就不大好操纵 Microsoft 的 Office X 程序,而较新的 Office 2004 已全部重写了 AppleScript 字典,很多方面都可与 Windows Office Automation 服务的字典相提并论了。

Linux 中的替代方案

虽然 Linux shell 支持复杂的命令行脚本,但仅限于可传给命令行程序的类型。在 Linux 中,没有一种统一的方式可像 Windows COM 自动化 和 macOS AppleScript 那样访问程序的内部类和命令。但各种桌面环境(GNOME/KDE)和应用程序框架,常会提供这种进程间通信的方法。在 GNOME 上,请参阅 Bonobo 组件。KDE 拥有 KParts 框架 DCOP。OpenOffice 有一个平台无关的 API 用于远程控制(google OpenOffice SDK)- 尽管可能得用另一种可作绑定的语言(如 Python)编写胶水代码。此外,某些应用程序还具备由特殊命令行选项激活的“服务器模式”,以便接受其他进程的控制。还有一种可能,也可以用 XReparentWindow(我认为是)将一个顶层 X 应用窗口“嵌入”另一个窗口中(Borland 就是如此调用 Kylix 文档浏览器的)。

与 Windows 一样,很多 macOS 和 Linux 程序都包含多个运行库文件(.dylib 和 .so 扩展名)。有时这些库会设计成可供其他程序调用。虽然这可以是在程序中加入外部程序某些功能的一种方式,但与运行及操纵外部程序并不完全相同。而只是链接并调用外部程序的库,与调用其他编程库无异。

Windows API 函数的替代方案

许多 Windows 程序大量用到了 Windows API。在跨平台应用程序中,不应再调用 Windows 单元的 Win API 函数了,或者应该用条件编译指令(如 {$IFDEF MSWINDOWS})包裹起来。

幸运的是,很多常用 Windows API 函数已在 lclintf 单元中以跨平台方式实现了。对于严重依赖 Windows API 的程序而言,这可能会是一种解决方案,尽管最好的解决方案是替换为 LCL 提供的真正跨平台的组件。比如可将 GDI 绘制函数替换为 TCanvas 对象方法。

键盘码

幸运的是,键盘按键编码检测方法(如 KeyUp 事件中的)是可移植的:请参阅 LCL Key Handling。

安装跨平台应用程序

请参阅 发布应用程序。

参见

外部链接


From the Free Pascal wiki, page "Multiplatform Programming Guide/zh CN", as the Internet Archive kept it on 2025-02-11, last changed on 2024-09-26. Copyright stays with the author who wrote it, according to the old wiki's Licensing page. If you have concerns about this material, message the admin.