65.9K
CodeProject 正在变化。 阅读更多。
Home

单实例应用程序行为,现已支持 Free Pascal

2016年4月3日

CPOL

10分钟阅读

viewsIcon

21504

downloadIcon

305

只有当实现所有三个功能时,单实例应用程序行为才能被认为是全面的:检测第二个实例、传递命令行和激活第一个实例

目录

 

 

引言

这是关于单实例应用程序行为的简短两篇文章系列中的第二篇。上一篇文章是关于 .NET 的解决方案;可以在此处找到:一站式解决单实例应用程序的所有三个功能,.NET

本文是我重拾我用 Borland Delphi 编写的非常旧的产品的结果,我希望将其移植到 Free Pascal 和 Lazarus LCL UI 库,并将其从仅限 Windows 推广到跨平台代码。正如我在上面引用的最近一篇文章中所述,我的一些单实例应用程序行为的解决方案不令人满意,包括 Pascal 的解决方案。此时,我已经有了同一篇文章中描述的 .NET 解决方案,因此我遵循我的 .NET 解决方案设定的模型开发了新的 Free Pascal 解决方案。结果非常简洁,因此我想分享它。

有关单应用程序实例工具的动机和需求解释,请参阅上面引用的文章。我写本文的假设是读者已经阅读了这篇文章并理解 .NET 实现背后的思想。

回到 Pascal!

为什么选择 Pascal?为什么选择 Free Pascal?因为这是我所知道的最好的原生代码编程跨平台技术。显然,老牌的 Delphi 遭受了巨大的衰落。这与许多事件有关,首先是前 Delphi 首席架构师 Anders Hejlsberg 领导下的 .NET 和 C# 的创建,他移居微软,Linux 版本 Kylix、其 CLX 跨平台库以及与 VCL 的不统一、某些较新版本中的严重错误、Delphi 被 Embarcadero 收购……大多数 Delphi 开发人员轻松迁移到了 .NET,因为 Delphi 的遗产和对 .NET 的直接影响使得 .NET 的功能对于有 Delphi 经验的人来说相当可预测。

与此同时,Object Pascal 和 Delphi Pascal 的发展历史创造了整个编程文化,一种高度文化、"学术性"的语言,安全高效,其表达能力优于许多现代语言,包括在许多方面优于 .NET 语言。仅举几例:集合、枚举类型(实际上可以枚举 并用于索引数组),以及重要的是,一个功能齐全的元类系统,遗憾的是,.NET 没有继承它,取而代之的是弱得多的、非类型的 System.Type type

Free Pascal 社区接过了失落的旗帜。我多年来没有过多关注它;该产品给我一种不成熟的印象,但最近几个版本真的给我留下了深刻的印象。首先,他们创建了一个真正的跨平台工具,包括一个巧妙的 UI 架构,称为 LCL。

支持的平台列表相当令人印象深刻

想法

与前一篇文章中的情况完全一样,主要思想是使用一些面向消息的 IPC 来传递命令行数据;并且相同的机制应该被重用,以检测应用程序第一个实例是否存在。

Free Pascal 捆绑了一个非常轻量级且简化的 IPC 库;它的简单性非常适合我们的目的。此功能实现为一对类:SimpleIpc.TSimpleIPCServerSimpleIpc.TSimpleIPCClient。连接失败时不需要捕获异常;服务器部分的存在已实现为布尔函数 SimpleIpc.TSimpleIPCClient.ServerRunning。请参阅 http://www.freepascal.org/docs-html/fcl/simpleipc/tsimpleipc.html

这些类操作的主要消息类型是字符串;它可以发送和接收,无需任何额外准备。然而,我们唯一需要的数据密集型消息不是字符串,而是字符串数组,我们需要将其序列化为字符串,然后再反序列化回数组。

用法

以下是其在入口点块中的用法:它以调用此类函数的单行额外行开始

if TSingleInstanceUtility.IsSecondInstance then exit;

但是,这还不是全部。当 TSingleInstanceUtility.IsSecondInstance 返回 false 时,第一个实例将继续执行。它应该准备一个服务器,用于稍后处理来自第二个实例的请求。这一次,应用程序需要创建一个 TSingleInstanceUtility 类的实例。此实例需要一个处理该实例唯一公共事件 OnCommandLineFromSecondInstance 的处理程序。该事件的处理程序接受一个字符串参数,用于从第一个实例传递命令行。这不是一个简单的字符串。这是一个以特殊方式序列化的已解析命令行参数的数组。因此,服务器部分需要将此字符串反序列化回字符串数组。这是通过调用类辅助方法 TSingleInstanceUtility.DeserializeCommandLine 来完成的。然后,服务器部分(第一个实例)应该一次性处理所有这些命令行参数(如果适用),并且可以选择性地激活自身(如果适用),这可能是在 Z 顺序中将应用程序的主窗口显示在其他应用程序窗口之上。在应用程序生命周期结束时,TSingleInstanceUtility 的实例应该被销毁。这可能是一个骨架形式:

procedure TFormMain.AcceptCommandLineFromSecondInstance(commandLine: string);
var
   files: array of string;
   // ...
begin
   Application.Minimize; // this is what really…
   Application.Restore;  // brings a form to top
   Application.BringToFront; // just in case, for other platforms
   files := TSingleInstanceUtility.DeserializeCommandLine(commandLine);
   // handle each file
   // ...
end;

constructor TFormMain.Create(TheOwner: TComponent);
begin
   inherited Create(TheOwner);
   SingleInstanceUtility := TSingleInstanceUtility.Create;
   SingleInstanceUtility.OnCommandLineFromSecondInstance := @AcceptCommandLineFromSecondInstance;
   // uninitialize form
   //..
end;

destructor TFormMain.Destroy;
begin
   SingleInstanceUtility.Free;
   inherited Destroy;
end;

表单激活的方式值得特别讨论。对于 Windows,激活表单的理想方式相当复杂。在我最近以跨平台方式移植到 Free Pascal 的原始 Delphi 应用程序中,这是基于原生 Windows API 的一个相当棘手的解决方案

procedure BringToFront(AppHandle: THandle);
//SA!!! July 2 2003: bugzilla#284 finally found solution:
var
   topWindow: HWnd;
begin
   if AppHandle = 0 then exit;
   topWindow := GetLastActivePopup(AppHandle);
   if (topWindow<>0) and (TopWindow <> AppHandle) and
      IsWindowVisible(topWindow) and IsWindowEnabled(topWindow) then
         SetForegroundWindow(topWindow);
end;

这里,需要 GetLastActivePopup 来覆盖应用程序在第二个实例启动时显示对话框的特殊情况。太糟糕了,在 LCL TFormTApplication 的跨平台实现中,没有一种方法可以像这样处理 Windows 部分的实现。当然,在隔离 Windows 实现部分的预编译器指令下,仍然可以直接使用 Windows API,但这仍然会损害应用程序的可移植性,尤其是其维护性。在纯粹的跨平台 LCL 调用中,Application.BringToFront 调用只是闪烁任务栏图标,引起对应用程序的注意,这与它的 .NET 类似物一样,显然不足以方便用户。通过最小化应用程序然后正常显示,可以显示表单,但这会产生一些几乎察觉不到的闪烁。

其他被认为适合的方法,如 TForm.ShowOnTopTForm.EnsureVisible(AMoveToTop: Boolean = True),都没有效果。所有这些在应用程序显示对话框时都不起作用。

这实际上不是 Lazarus 的问题。处理仅限 Windows 的代码(可以使用 {$IFDEF MSWINDOWS} 编译指令)时,尝试将窗口置于顶部表明 Windows API 的行为自 XP 以来已发生变化。问题在于,将窗口设置为前景窗口确实可以使用 SetForegroundWindow 实现,但在 Windows 7 中,这需要用户单击系统托盘,此时应用程序托盘图标会开始闪烁;其他函数如 BringWindowToTop 似乎被忽略了。当然,这对于目的来说相当不方便,因此最小化然后恢复窗口的“技巧”仍然是最好的。

实现

实现方式类似于该系列第一篇文章中描述的 .NET 实现,但明显更简洁。这里,我将只展示实用程序的核心方法,该方法尝试连接到服务器并在连接成功时发送命令行数据

class function TSingleInstanceUtility.IsSecondInstance: boolean;
var
    client: TSimpleIPCClient;
    commandLineMerged: string;
begin
   Result := false;
   client := TSimpleIPCClient.Create(nil);
   client.ServerID := GetServerID;
   try
      try
        if not client.ServerRunning then begin
             Result := false; exit;
         end {if};
         client.Connect;
         Result := true;
         if CommandLine.FileCount > 0 then begin
            commandLineMerged := SerializeCommandLine;
            client.SendStringMessage(commandLineMerged);
         end else
            client.SendStringMessage(EmptyString);
      except
         Result := false;
      end {exception};
   finally
      client.Free;
   end {exception};
end {TSingleInstanceUtility.IsSecondInstance};

自然,与 .NET remoting 一样,IPC 功能需要设置字符串属性 ServerID,该属性应在系统级别唯一。在我之前的文章中,我解释了我决定使用应用程序可执行文件的完整路径名。使用 SimpleIPC,事情,嗯,更简单。由于此字符串不用作带路径的 URI 的一部分,因此路径名不会干扰 URI 语法,因此在我的解决方案中使用它是合理的:ServerID 被分配给可执行文件路径,并且这就是 GetServerID 返回的值。在不同的平台上,此路径的外观会不同,但仍会在每个系统上保持唯一。

我的奖励

与我非常旧的命令行实用程序相关 O 的操作在“CmdLinePlus.pas”文件中找到。其用法从源代码中可以清楚地看出。有关此单元的可读文档,我建议将此文件重命名为 .HTML 并使用 Web 浏览器阅读(这是我发明的文档风格,但后来我发现其他开发人员也使用了完全相同的技术)。

它不像我最近的 .NET 作品基于枚举的命令行实用程序那样方便开发人员和全面,但功能齐全。好坏姑且不论,本文所述的单实例应用程序功能基于此命令行实用程序定义的通用文件格式。每个命令行参数,作为命令行中的字符串项,如 Delphi、Free Pascal 和 .NET 所理解的,都被解析并分为两种主要类型的参数:带可选值和状态的键,以及文件名。严格来说,文件名不一定就是文件名;这只是最常见的解释;这是一个不带键的参数。键是以下列字符之一开头的字符串:‘-’或‘/’,键本身,后跟可选值,用‘:’分隔,如常规 .NET 格式。

现在,限制是:单实例功能仅使用一组文件名,该文件名表示为字符串数组。只有这个字符串集被发送到应用程序的第一个实例。

当然,任何人都可以自由更改它并使用原始命令行。

有趣的是,TSingleInstanceUtility 将此字符串数组序列化,以将其作为单个消息发送给第一个实例;第一个实例需要将其反序列化,如用法代码示例所示,使用辅助函数。序列化被简化为将数组中的所有字符串连接成一个字符串,并用一个永远无法在命令行中输入的字符分隔;选择字符 #1。

演示/测试应用程序,构建和平台兼容性

我提供了演示/测试应用程序“SingleInstanceUtilityDemo”,当第二个实例启动时,它会显示一个选项卡控件;如果命令行中的某些项确实是现有文件,则它们会显示在每个单独的选项卡页面上。

对于 Windows,提供了单击批处理文件“build.bat”来构建所有代码。所有构建的输出都发送到与批处理文件相同的目录的子目录:“bin.Debug”、“bin.Release”(显然,我创建了两个自定义构建模式)以及中间目录“lib”,所有中间(对象/单元)文件都合并在此处。为了快速彻底地清理,提供了批处理文件“clean.bat”。

构建基于 Lazarus 应用程序“lazbuild.exe”。要构建代码,只需为所需平台安装 Lazarus 即可。可能需要根据安装目录修改“build.exe”中“lazbuild.exe”的路径。要在 IDE 中加载项目并进行构建,请单击“SingleInstanceUtilityDemo.lpi”。

对于交叉编译,您可以使用特定的 Lazarus 技术:重新构建 Lazarus,这可以在 IDE 中完成:菜单 Tools => Configure “Build Lazarus” 或 Build Lazarus…

重新构建的 Lazarus 可以发送到单独的目录,以后用于项目的交叉构建。

我还没有在 Windows 7 以外的任何平台上测试过该解决方案,但计划在 Linux 和 Android 上进行测试。如果有人愿意花精力测试此功能并报告任何问题,我将非常感激。

致谢

Free Pascal 和 Lazarus 团队
http://wiki.freepascal.org
http://www.freepascal.org/aboutus.var
http://www.lazarus-ide.org
http://www.lazarus-ide.org/index.php?page=about

Simple IPC:版权 © 2005 by Michael Van Canneyt。

© . All rights reserved.