zenn.devへ移行します

今後の技術ネタ記事をzenn.devへ移行しようと思います。

TOP

[.NET MAUI] Hello Worldアプリ実行時にWinAppRuntimeのエラー

仕事でWindowsアプリケーション(WPF/XAML/C#/.NET)をiOSに移植するプロジェクトが立ち上がったことに伴い、本ブログを5年ぶりに再始動させて情報発信しようと思う。移植にあたり、現行アプリの資産をできるだけ流用するべく、開発環境はWindowsのまま.NET MAUIを使用してiOS向けにビルドする、という前提。
さっそく、Visual Studio 2022をインストールして.NET MAUIアプリケーションのHello Worldをしよう、という段階で早速つまづきまくった。

Windows 11+Visual Studio 2022 17.4.3

この開発環境は自宅PCで2023年1月時点で作ったものだが、特につまづくことなくすんなりとHello Worldアプリ(新しいプロジェクト作成→.NET MAUIアプリケーション)をビルドして、「Windows Machine」で実行までできた。

Windows 10+Visual Studio 2022 17.8.1

会社の開発用PCにて、2023年11月21日にリリースされた最新版のVS2022をインストールしてHello Worldアプリのソリューションをビルドしたところ、ビルドは成功するものの、「Windows Machine」で実行しようとすると下記が出力されてアプリが実行されない事態に陥った。

警告 DEP0840: プロジェクト ‘MauiApp’ には、バージョンが ‘3000.934.1904.0’ 以上のパッケージ ‘MicrosoftCorporationII.WinAppRuntime.Main.1.3_8wekyb3d8bbwe’ と ‘MicrosoftCorporationII.WinAppRuntime.Singleton_8wekyb3d8bbwe’ が必要です。これらのパッケージは、初回実行時にインストールする必要があります。インストールしていない場合は、http://go.microsoft.com/fwlink/?linkid=2222757 にアクセスしてインストール手順をご確認ください。 MauiApp

Windows App SDK 用の最新のダウンロードより、「インストーラー (x64)」のリンクから最新版のWindows App SDKランタイムをインストールしたが、ビルド後の結果は変わらず。
約1時間ほど試行錯誤した結果、以下の手順で無事解決してHello Worldアプリが起動できるようになった。

  1. Windows App SDK 14をアンインストール
    Windows歯車アイコン→アプリ→インストールされているアプリ→Windows SDKで始まるものをひととおりアンインストール
  2. Windwos App SDK 13をインストール
    Windows App SDK 用の以前のバージョンのダウンロードより、ひとつ前のバージョンWindows App SDK 1.3のx64用ランタイムをインストール
  3. パッケージのバージョンを確認
    PowerShellを起動して「(get-appxpackage micro*win*appruntime* -AllUsers).packagefullname」を入力
    以下の通りであることを確認
    MicrosoftCorporationII.WinAppRuntime.Main.1.2_2000.802.31.0_x64__8wekyb3d8bbwe
    MicrosoftCorporationII.WinAppRuntime.Singleton_4000.1049.117.0_x64__8wekyb3d8bbwe

最初に出た警告をよく見れば…WinAppRuntime.Main.1.3…とあるので、最新バージョンのWindows App SDK 14に含まれるWinAppRuntime.Main.1.4だとダメということなんだろうけど、一般人の考えだと最新バージョン入れときゃ大丈夫、って思うよな。どうやらSDKやらパッケージやらが複雑な依存関係になっているようだ。そもそもこのエラーに行き着く以前に、インターネット接続していないオフライン環境でNuGetパッケージが取得できずにビルド失敗したり、などなど相当な苦労もした。開発環境の構築時点でつまづきまくりで先が思いやらる…

macOS Sonoma+Visual Studio for Mac 17.6.7+Xcode 15.0.1

この開発環境は特につまづきなくすんなりとビルド成功し、iOSシミュレータでの実行までも問題なく成功した。
ただしmacOSとXcodeのバージョン組み合わせによってはWindowsのVisual StudioからMacへリモートアクセスしてiOSシミュレータを起動したときに問題発生した。
この件は後ほど。

TOP

Firefox用WebExtensionsをChromeに対応させる

自作のXULベースのレガシー拡張機能 FoxAge2ch の WebExtensions 版 FoxAge5ch を2018年4月にリリースしたが、その後のバージョンアップにより、ソースコードを共通化したまま Chrome にも対応済みとなっている。 Firefox 用 WebExtensions を Chrome でも動作可能とするにあたり、両者のAPI差異を吸収するライブラリである browser-polyfill.js によるところが非常に大きいが、それ以外にもいくつか気付いた点があったため、ノウハウとしてまとめた。なお、前提として、ブラウザのバージョンは Firefox 60、 Chrome 67 とする。

browser API と chrome API の違い

Firefox と Chrome の一番大きな差異は、WebExtensions APIの名前空間が異なる点である。 Firefox の browser API は async / await 構文による Promise ベースのコーディングが可能である一方、 Chrome の chrome API はコールバックベースとなっている。ソースコードを共通化したまま Firefox と Chrome 両対応させるにあたり、当初はブラウザを判別してif文で分岐しながら頑張ろうとしたが、開発途中でこの差異を見事に吸収してくれるありがたいライブラリ browser-polyfill.js の存在に気付いた。

browser-polyfill.js の github にはライブラリ自体のソースコードが置かれているが、 WebExtensions で使用するには npm でパッケージ化する必要がある。現時点の最新バージョンがパッケージ化されたものも入手可能である。 WebExtensions 内での使い方としては、バックグラウンドスクリプトの場合 manifest.json の “background”.”script” にてロードし、サイドバーやポップアップなどの各種UI用HTMLの場合 script タグでロードすればよい。たったそれだけで、 Firefox 向けに記述した browser APIのコードが Chrome でも動作するようになる。(ただし一部例外あり)

ブラウザの判別

前項の browser-polyfill.js を使用することで概ね Firefox と Chrome のソースコードは共通化することができた。しかし、一部共通化できない部分やCSSをブラウザごとに若干調整する必要があり、ブラウザを判別してif文で分岐させる必要があった。ブラウザを判別する方法として、 browser-polyfill.js をロードしている前提であれば、下記の方法が手っ取り早い。ただし、これが最善かどうかは不明。

if ("sidebarAction" in browser) {
  // Firefox向けの処理
  ...
}
else {
  // Chrome向けの処理
  ...
}

background ページへのアクセス

Firefox では、サイドバーやポップアップなどの各種UI用HTML内で下記のようにして background ページへアクセスできた。

browser.runtime.getBackgroundPage(win => {
  window.MyBackgroundService = win.MyBackgroundService;
});

ところが、 browser-polyfill.js では上記の処理がエラーとなってしまうため、前述した方法でブラウザを判別し、 Chrome の場合は下記のようにした。

let win = chrome.extension.getBackgroundPage();
window.MyBackgroundService = win.MyBackgroundService;

キー押下イベントのキー判別

keydown, keypress イベントに対するイベントハンドラ内でどのキーが押されたか判別する際、 Firefox では KeyboardEvent.keyCode プロパティの値(数値)を event.DOM_VK_* 定数で判別することが可能。

if (event.keyCode == event.DOM_VK_RETURN) {
  // Enterキーが押された時の処理
  ...
}

しかし、 Chrome では event.DOM_VK_* 定数が定義されていないためエラーとなる。keyCode プロパティの使用はやめて、key または code プロパティを使用すべき。なお、 key と code プロパティはNumlockされている場合に値が異なる。

if (event.key == "Enter") {
  // Enterキーが押された時の処理
  ...
}

ミドルクリックに対するイベントハンドラ

マウスの中ボタンによるクリック(ミドルクリック)に対して何らかの処理を行う場合、 Firefox では click イベントに対してイベントハンドラを追加して event.button == 1 により中ボタンが押されたことを判別できた。しかし、 Chrome ではそもそも中ボタンによるクリックは click イベントが発生しない。そこで、 auxclick イベントに対するイベントハンドラを追加することで両ブラウザで動作可能となる。

event.targetで取得できるDOM要素の差異

document.addEventListener("click", onClick)のようにdocument全体にクリックイベントハンドラを追加しておき、イベントハンドラ側で event.target によりどのボタンが押されたかを判別する場合、ボタン(button要素)の中に img 要素を入れていたりすると、 event.target で取得できるDOM要素が Firefox と Chrome とで異なる。img 要素などでclickイベントを発生させないためには、下記のようなCSSを追加しておく。

img {
  pointer-events: none;
}

CSSの:any疑似クラス

CSSの :any 疑似クラスは将来的に :matches に代わるようだが、現時点で Firefox はベンダープレフィックス付きの :any のみ対応している。したがって、 Firefox / Chrome 両対応するには下記のように記述する必要がある。

:-moz-any(#foo, #bar) { ... }
:-webkit-any(#foo, #bar} { ... }

CSSのuser-select

各種UI用HTMLでマウスのドラッグなどで不必要な文字列選択を発生させなくするため、 user-select プロパティを設定すべきだが、現時点で Firefox ではベンダープレフィックスが必要となる。したがって、 Firefox / Chrome 両対応するには下記のように記述する必要がある。

html, body {
  user-select: none;
  -moz-user-select: none;
}

XMLHTTPRequest の User-Agent 改変

Chrome では下記のように XMLHTTPRequest によるHTTP要求の User-Agent ヘッダーを改変しようとするとエラーとなる。この処理は Firefox 限定とする必要がある。

var request = new XMLHTTPRequest();
request.setRequestHeader ("User-Agent", "...");

TOP

Tokyo WebExtensions Meetup #2

Tokyo WebExtensions Meetup #2 のプレゼン資料をこちらへアップしました。

TOP

Visual Tabs (Ver.0.1)

A WebExtension for Firefox, which enables to visualize tabs as thumbnails in sidebar.

Install

https://addons.mozilla.org/firefox/addon/visualtab/

TOP

FoxAge5ch (Ver.5.0)

インストール

https://addons.mozilla.org/ja/firefox/addon/foxage5ch/

新機能

  • タブの再利用(標準搭載)
  • HTTPS接続オプション(*.5ch.netに限定)

制限事項

  • ツールバーのカスタマイズからアイコンを配置可能ですが、サイドバーを開くだけで閉じることができません。
  • 2ペーン表示したときに境界線をドラッグ&ドロップで調整することができません。(おそらく実装は困難)
  • データの保存先はFirefoxプロファイルフォルダ内の「browser-extension-data\foxage2ch@xuldev.org」となり、これを変更することはできません。
  • 1日1回のjsonファイル自動バックアップは行われません。
  • 自動更新チェックなどいくつかの機能は不要と判断して削除しました。
  • GUIの見た目はWindowsに適合させており、MacやLinuxでは違和感があるかもしれません。

今後の実装予定

  • Firefox Syncによるデータの同期

その他

  • FoxAge2ch(レガシー版)からのデータ移行は、「設定」の「復元」から.jsonファイルを選択して実行してください。このとき、旧データの「.2ch.net」はすべて「.5ch.net」へ置換されます。
  • バグ報告は github (https://github.com/gomita/foxage5ch/issues) の方にして頂けるとありがたいです。

TOP

縦型2ペインのUI

WebExtensions にてサイドバーに縦型2ペインのUIを作成したい。
具体的には、上部にツールバーがあって、その下にリストボックスが2つ、という形式。

XULだとこんな感じ。
flex属性を指定しなければ上詰めで、flex属性を指定すれば同じ階層でflex属性値の比率にしたがって伸縮してレイアウトされる。

<toolbar>
  <toolbarbutton label="Button1" />
</toolbar>
<listbox id="mainList" flex="1">
  <listitem label="List1" />
  ...
</listbox>
<listbox id="subList" flex="1">
  <listitem label="List1" />
  ...
</listbox>

WebExtensions (HTML)であれば、まずFlexコンテナを作る。
Flexコンテナの高さをサイドバーぴったりにするため、height: 100vh;とするのがポイント。
Flexコンテナの直下に入れる要素はXULのflex属性と同じようにCSSのflexプロパティを設定する。

#container {
  height: 100vh;
  display: flex;
  flex-direction: column;
}
ul {
  margin: 0;
  flex: 1;
  overflow-y: auto;
}
<div id="container">
  <div id="toolbar">
    <button>Button1</button>
  </div>
  <ul id="mainList">
    <li>List1</li>
    ...
  </ul>
  <ul id="subList">
    <li>List1</li>
    ...
  </ul>
</div>

TOP

Thanks, and goodbye XUL!

先日 Tokyo WebExtensions Meetup #1 に参加したことをきっかけに、ここ1~2年終息状態だったアドオン開発を再開しようという気になった。「お前のアドオンをとっとと Firefox Quantum に移行しろ!」というメールも最近はあまり来なくなってしまったが、遅ればせながら自作XULアドオン(旧式の拡張機能)の今後について、方向性を整理しておこうと思う。

FireGestures

ユーザー数:199,320 ユーザー
個人的使用度:★★★★★

キーボードよりマウス派。とはいえ戻りたいときにいちいち戻るボタンをクリックしたり、タブを切り替えるためにタブバーから選択してクリックすることにはストレスを感じるので、マウスの右クリックをしながら小さな動きで様々なアクションができるマウスジェスチャーは便利だ。
XULとしてはタブブラウザ全体(xul:tabbrowser 要素)に対する mousedown, mousemove, mouseup などのDOMイベントを検知してマウスジェスチャーを実現可能だが、これを WebExtensions 流にやるなら content scripts で各ブラウザ内のWebページに対して同じことをすれば良さそう。すでにいくつかの新しい拡張機能が順調に開発を進めているようであり、十分な代替があるため FireGestures は開発打ち切り。
なお、自分が試したのは Gesturefy のみであるが、完成度が高く、FireGesuturesを少しだけ意識してくれつつもモダンなUIへと昇華されており、もはや自分自身手放せない状態である。

ScrapBook

ユーザー数:78,593 ユーザー
個人的使用度:★

Firefox黎明期から存在し、知名度的には高い。しかし、現在は自分自身がほぼ使用していないため、開発は打ち切り。かといって今まで保存したWebページが見られなくなってしまうのは困るかもしれないので、ツリー構造をHTML出力する機能を使ってからFirefox Quantumへアップデートしてください。なお、代替としては ScrapbookQ や Web ScrapBook といったものがあるもよう。

Tab Scope

ユーザー数:55,836 ユーザー
個人的使用度:★★

昔Operaにあった機能を参考とし、タブにマウスポインタを当てることでプレビューしてくれる。怪しい画像を開くときに役立つ機能。技術的には CSS transition を使いまくってヌルヌル動くのが気持ちよく、結構好きなアドオンだが、無くても別に困らない。ブラウザUI改造系は WebExtensions としては実現可能性が低いため、開発打ち切り。

Vertical Toolbar

ユーザー数:4,577 ユーザー
個人的使用度:★★★

これもOperaにあった機能で、ブラウザのツールバーをサイドバーの左側に縦型表示してくれる。横長ディスプレイ主流の時代では理にかなっていると思う。むしろ標準機能として取り入れてほしい。 WebExtensions としては今のところ実現不可だが、新しいツールバーを追加する、ツールバーを縦に表示する、といったAPIが追加されれば是非とも開発したい。

Flat Bookmarks

ユーザー数:1,767 ユーザー
個人的使用度:★★

ブックマークツリーで階層が深くなるとインデントが増す問題への対処として、スタック型UIにしてくれる。食べログで行きたい店を見つけて最寄り駅別にフォルダに分けてブックマークしたり、ブックマークを一番活用していた時代には役立った機能。しかし、最近はブックマークが面倒な生活になってしまった。たまにPocketするくらい。技術的には XUL overlay や eval 駆使による魔改造系アドオンであり、開発打ち切り。

FoxAge2ch

ユーザー数:1,516 ユーザー
個人的使用度:★★★★

2ちゃんねる(現5ちゃんねる)およびいくつかの互換掲示板の更新チェッカー。自分自身も常用しており、現在はこれだけのために Firefox ESR を別プロファイルで起動する不便さを強いられている。
サイドバーの独自UI、HTTPリクエストやJSONデータ管理などを中心としたバックグラウンドのサービスだけで完結しているので、WebExtensions への移行は比較的容易い。現在移行作業中で、必要最小限の機能は利用できるレベルまで達している。
というわけで、奇しくも一番ユーザー数が少ない FoxAge2ch だけが今のところ開発継続という結果となってしまった。

最後に

XULに出会ったのはおよそ14年前、研究室の先輩からの提案であり、それ以来長らく関わってきた中で得たプログラミング知識、仲間達との出会い、イベントや旅行、書籍執筆など、さまざまな思い出が詰まっている。XULの名前空間 http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul をそらで言えるようになったり、 Components.classes[“@mozilla.org/ …と呪文のごとくXPCOMを呼び出すことに妙な興奮を覚えたり、深い思い入れがある。
しかし、Mozilla(Firefox)としては FUEL、Jetpack、Rebootlessアドオンなど長い長い試行錯誤を経てようやく辿り着いた真打 WebExtensions であり、実際に触れてみるとその敷居の低さ、デバッグ環境の充実ぶり、モダンなプログラミングスタイルなど、ブラウザアドオン界の新たな幕開けを確信させるものであった。

というわけで、Thanks, and goodbye XUL!

TOP

Firefox への Feedly Cloud 用フィードリーダー追加

Google Reader から Feedly Cloud へ乗り換えしたので、 Firefox のブックマークメニューの「このページを購読…」からフィードを追加できるようにした。

手順

(1) Firefox で Feedly Could を開く。

(2) Feedly Could を開いたタブで、スクラッチパッド(「ツール」→「Web 開発」→「スクラッチパッド」)を開く。

(3) 以下のコードを貼り付けて、「実行」する。

navigator.registerContentHandler(
    "application/vnd.mozilla.maybe.feed", 
    "http://cloud.feedly.com/#subscription/feed/%s", 
    "feedly"
);

(4) タブ内の上部に「”feedly” (cloud.feedly.com) をフィードリーダーとして追加しますか?」と通知バーが表示されるので、「フィードリーダーを追加」ボタンをクリックする。

(5) 以降、何らかのフィードを開いてブックマークメニューから「このページを購読…」を選択すると、フィードリーダーの一覧に「feedly」が表示されるようになる。

navigator.registerContentHandler によるフィードリーダーの追加は、現在開いているページと同一ドメインでないと許可されないので、必ず上記(1)の手順で Feedly Could を開いておくことが重要。

別解

エラーコンソールを開いて下記のコードを実行する。この場合、前述のような同一ドメインの制約はない。

Components.classes["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
getService(Components.interfaces.nsIWebContentHandlerRegistrar).
registerContentHandler(
    "application/vnd.mozilla.maybe.feed",
    "http://cloud.feedly.com/#subscription/feed/%s",
    "feedly",
    null
);

参考

Firefox へのフィードリーダーの追加 | MDN

TOP

nsIZipWriter を使ってフォルダ丸ごと圧縮

前提

・Firefox 21~24.0a1
・変数 srcDir は圧縮元フォルダの nsILocalFile オブジェクト
・変数 zipFile は圧縮先ファイルの nsILocalFile オブジェクト
srcDir の中身のファイルをすべて圧縮して新規のアーカイブ zipFile を生成する

nsIZipWriter インスタンス生成

はじめに nsIZipWriter インスタンスを生成し、 open メソッドで圧縮先ファイルを開く。
圧縮率はデフォルト(レベル6)とする。

// |zipFile| is a nsILocalFile object corresponding to the zip file
var zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
const PR_WRONLY = 0x02;
const PR_CREATE_FILE = 0x08;
zipWriter.open(zipFile, PR_WRONLY | PR_CREATE_FILE);
var zipLevel = Ci.nsIZipWriter.COMPRESSION_DEFAULT;

フォルダ/ファイルのエントリ追加

srcDir を起点に、フォルダ内のファイルへ再帰的にアクセスする。

(function(dir) {
    var fileEnum = dir.directoryEntries;
    while (fileEnum.hasMoreElements()) {
        var file = fileEnum.getNext().QueryInterface(Ci.nsILocalFile);
        if (file.isDirectory()) {
            // [ToDo]            
            // go to sub folder recursively
            arguments.callee.call(this, file);
        }
        else if (file.isFile()) {
            // [ToDo]            
        }
    }
}).call(this, srcDir);
// |srcDir| is a nsILocalFile object corresponding to the folder where will be archived

変数 file がフォルダなら、 addEntryDirectory でエントリを追加する。第3引数 false だと即座に圧縮される。
変数 file がファイルなら、 addEntryFile でエントリを追加する。第4引数 false だと即座に圧縮される。
変数 file が通常のフォルダでもファイルでもなく、ショートカット(シンボリックリンク)などの場合は何もしない。

エントリ名は、圧縮元フォルダを起点にした圧縮元ファイルのパス。例えば、圧縮元ファイルのパスが「C:UsersHogeSourceDirectoryAAABBBccc.txt」なら、エントリ名は「AAA/BBB/ccc.txt」となる。フォルダの場合はエントリ名の末尾に「/」を付ける。

        var entry = file.path.substr(srcDir.path.length + 1).replace("", "/", "g");
        if (file.isDirectory()) {
            entry += "/";
            zipWriter.addEntryDirectory(entry, file.lastModifiedTime * 1000, false);
            // go to sub folder recursively
            arguments.callee.call(this, file);
        }
        else if (file.isFile()) {
            zipWriter.addEntryFile(entry, zipLevel, file, false);
        }

最後に圧縮先ファイルを閉じる。

zipWriter.close();

processQueue を使って後からまとめて圧縮

addEntryDirectory, addEntryFile の最後の引数を true にすると、その時点では圧縮されず、後で processQueue メソッド呼び出し時にまとめて圧縮される。

        if (file.isDirectory()) {
            entry += "/";
            zipWriter.addEntryDirectory(entry, file.lastModifiedTime * 1000, true);
            arguments.callee.call(this, file);
        }
        else if (file.isFile()) {
            zipWriter.addEntryFile(entry, zipLevel, file, true);
        }

processQueue の第1引数には nsIRequestObserver オブジェクトを渡す。
nsIRequestObserver は最初のファイル圧縮前に呼び出される onStartRequest と、最後のファイル圧縮後に呼び出される onStopRequest メソッドを持つ。

    zipWriter.processQueue({
        onStartRequest: function(aReuqest, aContext) {
        },
        onStopRequest: function(aRequest, aContext) {
            zipWriter.close();
        },
    }, null);

TOP