12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Delphi] macOS で Browser を開く時 POSIX の機能を使うと言ったな。あれは嘘だ。

Last updated at Posted at 2021-02-01

macOS でブラウザを開く

macOS でブラウザを開きたい!と思って検索すると下のようなコードが引っかかると思います。

_system(PAnsiChar('open ' + AnsiString(URL)));

macOS は POSIX 準拠 OS なので、POSIX の system 関数経由で open コマンドが使えます。
つまり、macOS では POSIX API を使えば簡単にブラウザが開けるのでこれでええじゃろ、という事です。

でも!本当に大丈夫なんでしょうか!

実験

さて、それでは Windows と macOS で下記の URL にアクセスしてみます。

URL
https://google.co.jp/$1234

これは存在しないパスです。
Google は存在しないパスにアクセスするとエラーページでこんなパスないよ~って表示してくれるので、今回はその機能を利用させて貰います。
(この機能を利用するだけで、存在しないパスだけの問題ではないです)

上記の URL にアクセスするコードは以下です。

ブラウザを開く
procedure TForm1.Button1Click(Sender: TObject);
begin
  const URL = 'https://google.co.jp/$1234'; // URL

  // Windows 版
  {$IFDEF MSWINDOWS}
  ShellExecute(0, 'open', PChar(URL), nil, nil, SW_SHOW);
  {$ENDIF}

  // macOS 版
  {$IFDEF OSX}
  _system(PAnsiChar(AnsiString('open ' + iURL.QuotedString('"'))))
  {$ENDIF}
end;

Windows 版で使っている ShellExecute も system 関数と大体同じ機能を持つ API ですね。

実行結果

Windows での結果

image.png

/$1234 なんていうパスないよ~って言ってますね。
期待通りです。

macOS での結果

image.png

!?
/234 なんてパスないよ~って言っています!
期待値と違いますね!?

macOS の system 関数にはバグがある

上の結果の通り macOS の system 関数にはバグがあります。
確認した限り

  • /$
  • /%

というパスの時に/の後ろの文字が2つ消えるという物です。

https://google.co.jp/$1234

/の後ろの文字が2つ消える ($ と後ろの文字が消える)

macOSのsystem関数を通した結果
https://google.co.jp/234

/$ はまだしも URLEncode した URL では、/% なんて容易にありうるパスです。
これは致命的です。

この動作は macOS の system 関数が愚直に shell を呼び出しているせいで $ などの特殊文字が効いてしまっているからかもしれません。
試しに Terminal で open "https://google.com/$1234" とすると別の結果1が得られるため、system 関数に問題がありそうです。

macOS での正しいブラウザの開き方

ずばり↓こうです。

// uses には Macapi.AppKit, Macapi.Helpers を追加する
var Workspace := TNSWorkspace.Wrap(TNSWorkspace.OCClass.sharedWorkspace);
Workspace.openURL(StrToNSUrl(iURL));

NSWorksupaceopenURL メソッドを使います。
たったこれだけ。
POSIX に負けず劣らず簡単ですね。

まとめ

郷に入っては郷に従え、macOS ネイティブの API がある場合はそれを使った方が良さそうです。

おまけ

Windows, macOS, Android, iOS, Linux 全てに対応したコードです。

  • Windows は ShellExecute を使い、失敗した場合は WinExec で explorer を呼び出します。
  • macOS は前述の通り、NSWorkspace.openURL を使っています。
  • Android は暗黙的 Intent を使っています。
  • iOS は UIApplication.openURL が deprecated になっているため、自前で openURL:options:completionHandler: を定義して使っています(定義さえしてしまえば OS の API を自由に使えるのも Delphi の良いところですね)
  • Linux は xdg-utils の xdg-open を使っています。
(*
 * Browser Utils
 *
 * PLATFORMS
 *   Windows / macOS / iOS / Android / Linux (needs xdg-utils)
 *
 * USAGE
 *   OpenBrowser with URL
 *     TBrowserUtils.Open('URL');
 *
 * LICENSE
 *   Copyright (c) 2015, 2021 HOSOKAWA Jun
 *   Released under the MIT license
 *   http://opensource.org/licenses/mit-license.php
 *
 * HISTORY
 *   2015/11/27 Ver 1.0.0
 *   2021/01/20 Ver 1.0.1  macOS: _system open -> NSWorkspace.openURL
 *   2021/01/30 Ver 1.0.2  iOS: openURL -> openURL:options:completionHandler:
 *   2021/01/31 Ver 1.1.0  Global procedure -> record
 *
 * Programmed by HOSOKAWA Jun (twitter: @pik)
 *)

unit PK.Utils.Browser;

interface

type
  TBrowserUtils = record
  public
    class procedure Open(const iURL: String); static;
  end;

implementation

uses
  System.SysUtils

  {$IFDEF MSWINDOWS}
    , Winapi.Windows
    , Winapi.ShellAPI
  {$ENDIF}

  {$IFDEF OSX}
    , Macapi.AppKit
    , Macapi.Helpers
  {$ENDIF}

  {$IFDEF ANDROID}
    , Androidapi.JNI.Net
    , Androidapi.JNI.App
    , Androidapi.JNI.GraphicsContentViewText
    , Androidapi.Helpers
    , FMX.Helpers.Android
  {$ENDIF}

  {$IFDEF IOS}
    , iOSapi.Foundation
    , iOSapi.UIKit
    , Macapi.Helpers
    , Macapi.ObjectiveC
    , FMX.Helpers.IOS
  {$ENDIF}

  {$IFDEF LINUX}
    , Posix.StdLib
  {$ENDIF}
  ;

{$IFDEF IOS}
type
  // openURL は iOS 10.0 で deprecated になり
  // openURL:options:completionHandler: に変更しなくてはならなくなった
  // だが 2021/2 現在の Delphi 10.4.1 の UIApplication では
  // openURL:options:completionHandler: は定義されていないため自分で定義する
  UIApplicationEx = interface(UIApplication)
    [MethodName('openURL:options:completionHandler:')]
    function OpenURLoptionsCompletionHandler(
      url: NSURL;
      options: Pointer;
      completionHandler: Pointer): Boolean; cdecl;
  end;
  TUIApplicationEx =
    class(TOCGenericImport<UIApplicationClass, UIApplicationEx>)
    end;
{$ENDIF}

class procedure TBrowserUtils.Open(const iURL: String);
begin
  {$IFDEF MSWINDOWS}
    if (ShellExecute(0, 'open', PChar(iURL), nil, nil, SW_SHOW) < 32) then
      WinExec(PAnsiChar(AnsiString('explorer ' + iURL)), SW_SHOW)
  {$ENDIF}

  {$IFDEF OSX}
    var Workspace := TNSWorkspace.Wrap(TNSWorkspace.OCClass.sharedWorkspace);
    Workspace.openURL(StrToNSUrl(iURL));
  {$ENDIF}

  {$IFDEF ANDROID}
    var Intent :=
      TJIntent.JavaClass.init(
        TJIntent.JavaClass.ACTION_VIEW,
        StrToJURI(iURL));

    {$IF RTLVersion < 30}
      SharedActivity.startActivity(Intent);
    {$ELSE}
      TAndroidHelper.Activity.startActivity(Intent);
    {$ENDIF}
  {$ENDIF}

  {$IFDEF IOS}
    var App := TUIApplicationEx.Wrap(TUIApplication.OCClass.sharedApplication);
    var URI := TNSURL.Wrap(TNSURL.OCClass.URLWithString(StrToNSStr(iURL)));

    if TOSVersion.Check(10, 0) then
    begin
      var Dic := TNSDictionary.OCClass.dictionary;
      App.OpenURLoptionsCompletionHandler(URI, Dic, nil);
    end
    else
      App.OpenURL(URI);
  {$ENDIF}

  {$IFDEF LINUX}
    _system(PAnsiChar('xdg-open '+ AnsiString(iURL.QuotedString('"'))));
  {$ENDIF}
end;

end.
  1. 404 Error になるもののトップページが表示される

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?