Multiplatform Programming Guide/zh CN

From Free Pascal wiki
Jump to navigationJump to search

Deutsch (de) English (en) español (es) français (fr) 日本語 (ja) polski (pl) русский (ru) 中文(中国大陆) (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 声明了以下常量,用于处理文件或文件夹。

  • PathSepPathSeparator:多路径并列时的分隔符(';' 等)
  • PathDelimDirectorySeparator:各平台的目录分隔符('/'、'\'等)
  • LineEnding:行结束符(#13#10 是 CRLF、#10 是 LF等)

另一个需注意的要点是文件系统是否大小写敏感。 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 字符串。如下所示:

// 在 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 系统有不同的做法。

  • Windows 允许文件名为空。这就是为什么在 Windows 中 FileExistsUTF8('..\') 可用于检测父目录中是否存在空名文件。
  • 在 Linux/Unix/类 Unix 系统中,空文件映射为目录,目录视作文件处理。这意味着在 Unix 中,FileExistsUTF8('../') 将检测父目录是否存在,通常结果都是 true。

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

  • Windows:'C:\' 与 'C:\\' 不同
  • 类 Unix 系统:路径 '/usr//' 等同于 '/usr/',如果 '/usr' 是个目录,则也等同于 '/usr//' 和 '/usr/'。

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

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 系统中,只要用 FileExistsUTF8 即可。比如 FileExistsUTF8('/home/user/') 在 /home/user 存在时会返回 true。
  • 在 Windows 中,必须用 DirectoryExistsUTF8 函数,还必须预先删除路径分隔符,比如用 ChompPathDelim 函数。

类 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 单元中有一个猜测文本编码的函数:

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

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

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 编码,可用如下代码:

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;

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

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

配置文件

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

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

ConfigFilePath := GetAppConfigFile(False);

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

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 返回的目录中创建一个目录,然后将配置文件存于其中。

Light bulb  Note: 普通用户对 /etc 目录没有写入权限。只有具备管理员权限的用户才行。

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

Windows 98 下 FPC 2.2.0 的输出:

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 的输出:

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 的输出:

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):

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

FreeBSD 12.1 下 FPC 3.0.4 的输出:

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

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

WriteLn(GetAppConfigFile(True)); 

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

WriteLn(GetAppConfigFile(True,True));
Light bulb  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 时,才对这些目录具有写入权限),不过也会用文件夹重定向机制来兼容老程序或编码错误程序。对应用程序目录中的数据可以只读(不写),但不推荐。

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

    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”,则:

  • 可执行文件为“path/MyApp.app/Contents/MacOS/myapp”
  • 资源目录为“path/MyApp.app/Contents/Resources”

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

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

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

或任何用到它的函数来确定可执行文件的位置,因为这是 DOS-Windows-OS/2 系统的约定,在概念上存在一些问题,且在其他平台上无法模拟解决。在 UNIX 平台中,

paramStr(0)

唯一能保证返回的是程序启动时的名称。

程序所在的目录和二进制文件的真实名称(若以符号链接启动),用 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 贡献于论坛

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 都能有效,请使用以下代码:

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

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

字节序

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

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

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

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

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

Libc 和其他特殊单元

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

Assembler

请避免使用 汇编语言

编译器 define 指令

{$ifdef CPU32}
...编写运行于 32 位处理器下的代码
{$ENDIF}
{$ifdef CPU64}
...编写运行于 64 位处理器下的代码
{$ENDIF}

项目、包和搜索路径

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

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

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

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

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

平台依赖的单元

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

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

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

请参阅平台依赖的单元

平台依赖的搜索路径

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

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

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

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

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

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

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

  • LCLWidgetType:win32、 gtk、gtk2、qt、carbon、fpgui、nogui
  • TargetOS:linux、win32、win64、wince、freebsd、netbsd、openbsd、darwin(等等)
  • TargetCPU:i386、x86_64、arm、powerpc、sparc
  • SrcOS:win、unix

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

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

$(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

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

// 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}

和:

// 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 时即可使用上述格式设置,如下所示:

// 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),就应牢记这些不兼容的地方。

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

那就做个测试吧:

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 默认的屏蔽设置:


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

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

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

var
  X, A, B: Double;
// ...

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

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

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

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;
// ...

或者可以:

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):

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

而应使用辅助变量:

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 版本):

 tell application "NeoOffice"
   launch
 end tell

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

 Shell('myscript.applescript');

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

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

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

 Shell('open -a NeoOffice');

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

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

 fpsystem('open -a mail "mailto:ss4200@invalid.org"');

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

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

 fpsystem('open "mailto:ss4200@invalid.org"');

如果用了 fpsystemshell(可互替),请不要忘记在 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

安装跨平台应用程序

请参阅 发布应用程序

参见

外部链接