ラベル Windows開発 の投稿を表示しています。 すべての投稿を表示
ラベル Windows開発 の投稿を表示しています。 すべての投稿を表示

2024年4月7日日曜日

C# FormアプリでMicrosoftのライブラリだけでheic画像表示

WPFではサポートされてもFormアプリでは使えない機能があります。

Imageのheic対応もそのひとつで、次のようなコードでheicファイルを表示しようとするとメモリ不足のエラーが発生します。

pictureBox1.Image = Image.FromFile(mediaItem.FilePath);

Webでheic表示関連を検索するとMagick.NETを使う例が見つかりますが、Microsoftのライブラリだけで行う方法はまだ見つかりませんでした。Magick.NETは様々なフォーマットを扱える優れたものだと思いますが、dllをアプリに含めると、小さいなアプリでは本体よりMagick.NETの方が大きくなってしまいます。heicサポートの追加だけが目的な場合はあまり好ましくないので、Microsoftのライブラリだけで行う方法をいくつか試してみました。

なお、前提としてWindows 10, 11にHEIF画像拡張をインストールしてあるものとします。HEIF画像拡張がインストールされていない場合はMicrosoft Store から ダウンロードできます。

方法1
BitmapImage
BitmapEncoderを使用する。
今のところ、これが一番良好な結果になっています。
次のようなステップで行います。

System.Windows.Media.Imaging.BitmapImageをファイルから作る。BmpBitmapEncoderでMemoryStreamに書き出す。
MemoryStreamからSystem.Drawing.Imageを作る。
pictureBox1.Imageにセットする。

コードは次のようになります。

            BitmapImage bImage = new BitmapImage();
            bImage.BeginInit();
            bImage.CacheOption = BitmapCacheOption.None;
            bImage.DecodePixelHeight = height;
            bImage.DecodePixelWidth = width;
            bImage.UriSource = new Uri(FilePath, UriKind.Absolute);
            bImage.EndInit();
            using (MemoryStream ms = new MemoryStream())
            {
                BitmapEncoder enc = new BmpBitmapEncoder();
                enc.Frames.Add(BitmapFrame.Create(bImage));
                enc.Save(ms);
                pictureBox1.Image = Image.FromStream(ms);
            }

ひとつの静止画を扱うなら、これで十分でしょう。イメージのサイズを設定できるので、メモリ消費も必要最低限に抑えられ、スピードもこれ以上早くするのは難しいでしょう。

また、書き出し先をファイルにすればフォーマット変換にも使えます。

System.Windows.Media.Imagingのための参照追加 は次のとおりです。

プロジェクトの参照右クリック⇒参照の追加⇒アッセンブリ
PresentationCoreをチェック⇒OK

方法2
BitmapDecoder
BitmapEncoderを使用する。

BitmapDecoderの使い方はよくわかっていませんが、複数のFrameを持ち、Animationなども扱えます。複雑な画像表示を行えるようですが、ひとつの静止画だけを扱うなら方法1で十分そうです。もしかしたら有用かもしれないので紹介しておきます。

ステップは方法1のBitmapImageがSystem.Windows.Media.Imaging.BitmapDecoderに置き換わっただけです。

コードは次のようになります。

            BitmapDecoder uriBitmap = BitmapDecoder.Create(
                new Uri(FilePath, UriKind.Absolute),
      BitmapCreateOptions.None,
                BitmapCacheOption.Default);
            using (MemoryStream ms = new MemoryStream())
           {
               BitmapEncoder enc = new BmpBitmapEncoder();
               enc.Frames.Add(uriBitmap.Frames[0]);
               enc.Save(ms);
               pictureBox1.Image = Image.FromStream(ms);
           }

調べた限りでは、この流れでは画像サイズを指定できません。そのため、高解像度画像ではメモリ消費が多くなり、スピードも少し遅くなります。

方法3
 AxWMPLib.AxWindowsMediaPlayerを使用する。

MediaPlayerは静止画も表示でき、HEIF 画像拡張インストールされていればheicフォーマットも表示できます。ちなみにHEVCビデオも再生できます。静止画を扱う場合は uiMode="none"が良いでしょう。

メリットとしては、まずは静止画も動画も同じインタフェイスで扱えることです。また、対応していないフォーマットや壊れたファイルのエラー対応もMediaPlayerまかせにできます。

これまでのFormアプリと同じ要領で使え、次の要領でコントロールにファイルのパスを設定するだけなので、お手軽といえます。

(AxWMPLib.AxWindowsMediaPlayer)player.URL = filePath;

VisualStudioでMediaPlayerを使う方法は次のリンクを参照してください。
Microsoft Visual Studio で Windows メディア プレーヤー コントロールを使用する
https://draft.blogger.com/blog/post/edit/6538117324271148932/209217070553378409#

デメリットとしては、方法1,2に比べると遅いこと、メモリ消費量も特に高解像度のheicの場合は多くなります。

HEIFコーディックの有無チェック 
Magick.NETと違い、HEIFコーディックがないとエラーになります。
以下の要領でHEIFコーディックの有無がチェックできます。 

bool HeifDllExists = Registry.GetValue(@"HKEY_CLASSES_ROOT\CLSID\{E9A4A80A-44FE-4DE4-8971-7150B10A5199}\InprocServer32", null, null) != null;

参照:HEIF Format Overview
https://learn.microsoft.com/ja-jp/previous-versions/windows/desktop/legacy/mt846532(v=vs.85)




2023年9月7日木曜日

C# メール送信プログラムの「SSPIへの呼び出しに失敗しました。内部例外を参照してください。」エラー対応

 数年前に作ったメール送信機能のあるプログラムを作り変えようとしたら、「SSPIへの呼び出しに失敗しました。内部例外を参照してください。」エラーが発生した。

以前作ったとき、.NETのSmtpClientがexplisit SSLのみの対応の、私が使っているjcomのサーバのimplicit SSLで発生が発生したため、そのとき見つけたAegisImplicitMailを利用していた。

今回のエラーはSmtpSocketConnection.Openメソッドの中の

    sslStream.AuthenticateAsClient(host)

で発生した。

検索してみたところ、参考になる記述を発見。

 

PowershellでSSL証明書情報取得時にTLSエラーにハマッた件    https://qiita.com/pizza_slice/items/00d00fd900bb3f0fd697

Windows Server 2016

Window 10 21H2 だったら正常にコマンドが通る。
恐らくこれもOS依存のTLSのバージョンの問題。

# 下記を参考にしてオーバーロード
# AuthenticateAsClient(String, X509CertificateCollection, SslProtocols, Boolean)
# Tls12:3072 TLS1.2セキュリティ プロトコルを指定します$stream.AuthenticateAsClient($commonName, $null, "3072", $false)


同じ理由かもしれないと、AuthenticateAsClientをこれに対応するメソッドに置き換えてみる。

    sslStream.AuthenticateAsClient(host, null, (SslProtocols)3072, false);

するとエラーが発生しなくなった。
(SslProtocols)3072 としたのは、プロジェクトの.NETのバージョンのせいかと思うが、SslProtocols 列挙型にTls12が含まれていなかったため、強引にそのInt値をcastしたからです。
列挙型の名前とint値の対照は次のページを参照してください。

SslProtocols 列挙型

追記

エラーが発生したのは.NET4.0でした。4.5(以上)に変更するとSslProtocols .Tls12が定義されており、上記のintからのcastは不要になります。

なお、このプロジェクトで使用しているAegisImplicitMailの.NETバージョンの関係で、4.5より新しいバージョンでは試していません。SslProtocols .Defaultの値にTls12が含まれていれば、AuthenticateAsClient(host)でもエラーが発生しなくなるでしょう。




2022年8月22日月曜日

WiX ToolSet v3.xでの.NET Frameworkの必須バージョンの追加

WiX ToolSet v3.xは.NET Fraomeworkのバージョンチェックのための定義を4.6.2までは含んでいるが、それ以降のバージョンについては定義の追加が必要になる。

WiX ToolSetで定義済みの.NETバージョンの場合は、
このページに簡潔な説明がある。

また、次のQ&AでDOT.NET 4.8の定義を追加して、同様のバージョンチェックを行う方法が紹介されている。
NetFxExtension should support .net 4.8 
  <?define NetFx480MinRelease = 528040 ?>でNetFx480MinReleaseの名前で値を定義
  
WiX ToolSetで定義済みの.NETバージョン定義を PropertyRef で取得できる。
例:<PropertyRef Id='WIXNETFX4RELEASEINSTALLED'/>

この値はレジストリに登録されている.NET release keyとなる。

この値と.NET のバージョンの最小値(NetFx480MinRelease=528040)を比較することで、インストールされている.NET Frameworkが要件を満たしているか判別できる。

設定例
次の例ではWIXNETFX4RELEASEINSTALLED とバージョンの最小値を直接比較する。
<PropertyRef Id='WIXNETFX4RELEASEINSTALLED'/>
<Condition Message='This setup requires the .NET Framework 4.7.2 (or greater) to be installed.'>
<![CDATA[Installed OR (WIXNETFX4RELEASEINSTALLED >= "#461808")]]>
</Condition> 次の例ではNetFx480MinReleaseという名前でバージョン番号を定義し、これと比較してWIX_IS_NETFRAMEWORK_480_OR_LATER_INSTALLEDの値を設定する。
<?define NetFx480MinRelease = 528040 ?>
<PropertyRef Id="WIXNETFX4RELEASEINSTALLED" /> <Property Id="WIX_IS_NETFRAMEWORK_480_OR_LATER_INSTALLED" Secure="yes" /> <SetProperty Id="WIX_IS_NETFRAMEWORK_480_OR_LATER_INSTALLED" Value="1" After="AppSearch"> WIXNETFX4RELEASEINSTALLED >= "#$(var.NetFx480MinRelease)" </SetProperty>

2022年8月19日金曜日

自作アプリがウイルス(トロイの木馬)扱いになった ⇒ 解決?

今度は「Behavior:Win32/Hive.ZY」が見つかった警告された。

やれやれまたか...で、こんどは自作アプリを疑う前にDefenderに関する書き込みをチェック。すると「窓の杜」からニュースが流れていた。

マルウェア「win32/hive.zy」が検出されるトラブル ~「Microsoft Defender」ウイルス対策の誤検知か

日本時間(2022年)9月4日より、内部で「Chromium」を利用するアプリ(「Google Chrome」や「Microsoft Edge」、「Spotify」など)を起動するたびに「Microsoft Defender」ウイルス対策が「win32/hive.zy」というマルウェアを検知する現象が発生しているようだ。編集部でも確認をした

このページのお勧めに従って[ウイルスと脅威の防止の更新]を行った。さっそくアップデートがあったようだ。

------------

解決と書いたが、しばらくして再発。

AxWindowsMediaPlayerを使用しており、コントロールを追加するとAxInterop.WMPLib.dll、Interop.WMPLib.dllが追加される。このあたりを疑って試しているうちに、また安定し始めた。

以前にウイルス扱いとなったバージョンも今は問題ない。

どうやらDefenderの定義ファイルが修正されたようだ。

------------

先日「自作アプリがウイルス扱いになったのがWIndows 11再インストールで解決した」と書き込みをしたが、その後システムドライブバックアップ中に検疫にひっかかり、再度トロイの木馬扱いとなった。

ソースコードで関連ありそうなメソッド呼び出し順次止めて試したところ、Assemblyからcopyrightの文字列を取得している箇所を削除することで検疫をパスするようになった。

Attribute copyright = Attribute.GetCustomAttribute(Assembly.GetExecutingAssembly(), typeof(System.Reflection.AssemblyCopyrightAttribute));

ただし、このメソッド呼び出しは別アプリでも使用しており、なにかしら別要因との組み合わせがあると思われる。

------------

2022年8月初旬に自作アプリ(RenameMe)がトロイの木馬(Trojan:Script/Wacatac.H!ml)扱いになって、Defederで検疫されてしまった。

表示されたダイアログをコピーしておかなかったが、次のようなメッセージが表示され、挙句Defenderから脅威が見つかったと警告された。

・This applicaion could not be started.
・このアプリを実行すると、PC に問題が起こる可能性があります。

アプリ本体はDOT.NETのライブラリとメディアプレーヤのWMPLibで、いずれもMicrosoft製のものしか使用していない。

インストーラはオープンソースのWixTookSetを利用しているが、これはMicrosoft製のツールを引き継いだもので、Windows Installer (MSI) パッケージを作成するが実際のインストールはWindowsの機能で行うものだ。また、インストールしたアプリだけでなくReleaseビルドしたexeを起動しても「このアプリを実行すると、PC に問題が起こる可能性があります。」となるのでインスーラは関係なさそう。

今まで動いていたものを突如トロイの木馬扱いにするのだから、これは誤検知に相違ないのだが、といってアプリが動かないのは困る。

まずはアプリをビルドしなおしてみる。やはりダメなので次のことを順次試して、アプリケーションの削除/インストールを繰り返してみる。

・DOT.NETのバージョンを4.7.2から4.8に上げる
・WixTookSetを3.11.2に上げる

DOT.NET Framework4.8の再インストールを試みるが、これは既にインストール済みと拒否される。

これでも同じ現象が続くので、Windows Updateで更新状態をチェックすると、KB5016629のインストール待ち状態だった。これのインストールを試みるが失敗。

次のようなコマンドを実行するがエラーで成功しない。

dism /Online /Cleanup-Image /ScanHealth
dism /online /cleanup-image /startcomponentcleanup
sfc /scannow

エラーメッセージは次のようなもの。
・指定したバッファーに誤った形式のデータが含まれています。
・Windows リソース保護は要求された操作を実行できませんでした。

当然ながらWindows Updateは再度失敗。
オフライン更新も試みるが同じエラーで失敗。

いささか行き詰まり状態になり、復元ポイントやバックアップで前の状態に戻すことを考え始めたが、Window 11では個人データだけでなくアプリも残したのまま上書きインストール可能というのを発見し、これを試みることにする。Windows 10でも上書きインストールできたんですね。

参照: Windows11 上書きインストールで現状を全て保持して修復

isoをダウンロードしインストールを開始すると、なんとTPM2.0が有効になっていないという。UEFI(BIOS)画面でチェックすると、確かに無効になっていた。しばらく前にBIOSをアップデートしたので、そのときに初期状態に戻っていたようだ。Windows 11はTPM2.0必須ではあるが、インストール後これが無効でも起動するのですね。

ついでに高速スタートアップが無効になっていることを確認。

TPM2.0を有効にし、再度上書きインストールを試み、成功。

再起動後にWindows Updateを見るとKB5016629の前のKB5015732、KB4023057も待ち状態になっていた。

KB4023057は「.NET Framework 3.5 および 4.8 の累積的な信頼性の向上が含まれています」となっており、どうもこれが関係しているように思われる。

その後、アプリを再ビルド、再インストールし、今のところ安定して動作している。
⇒その後システムドライブ バックアップ時に再発

振り返ってみると、つぎのような順序で問題が発生したのではないかと思われる。

・しばらく前にWindowsの動作が重くなり、BIOSのアップデートを行った。(実際はBIOSの問題ではなくIMEをGoogle日本語入力に置き換えることで解決した。)

・BIOSアップデート時に設定が初期化され、TPM2.0がオフになった。

・Windows 11は動作しつづけたが、もしかしたらTPM2.0オフが一因でWindows Updateで更新失敗が発生。もしかしたらDOT.NET 4.8が関係してウイルス検知を誘発。

・KB5015732、KB4023057いずれかの更新失敗のため、KB5016629も更新失敗。

・このときのゴミでdism、sfcコマンドでもエラーが発生。

2021年6月14日月曜日

C# TagLib#でwavファイルのタグを設定する方法

TagLibでmp3のタグ編集アプリを作りつつ、ついでにwavのタグ設定を試してみました。

mp3で文字化けに悩まされたましたが、wavでもやはり発生しました。

wavの場合の問題は、WindowsはwavのタグをShift-JISで読み書きするのに対し、TagLibはUTF8で読み書きすることです。

TagLibはファイルから読み込んだバイト列をByteVectorオブジェクトに格納し、これをAPIを介してStringにしています。

なので、そのAPIを介さずに、TagLibが保持しているバイト列をShift-JIS EncodingでString化し、Saveする前にバイト列を直接セットすれば文字化けを回避できます。

TagFileを作るときはファイルの種類を意識する必要はありません。

    TagLib.File TagFile = TagLib.File.Create(FilePath);

TagFileからTagを取得するときは、TagLib.TagTypes.RiffInfoを使います。

    TagLib.Riff.InfoTag tag = TagFile.GetTag(TagLib.Riff.InfoTag, true);

次の要領でShift-JIS Endodingでバイト列をString化します。

    Encoding SjisEnc = System.Text.Encoding.GetEncoding("shift-jis");
    String GetStringFromSjisPropery(TagLib.Riff.InfoTag tag, TagLib.ByteVector id)
    {
        TagLib.ByteVectorCollection vals = tag.GetValues(id);
        foreach (TagLib.ByteVector v in vals)
        {
            byte[] b = v.Data;
            if (b.Length > 0)
            {
            return SjisEnc.GetString(b);
            }
        }
        return null;
    }

Artist, Album, Titleのidは次のようになります。

    ByteVector idArtist = new ByteVector(Encoding.ASCII.GetBytes("IART"));
    ByteVector idAlbum = new ByteVector(Encoding.ASCII.GetBytes("IPRD"));
    ByteVector idTitle = new ByteVector(Encoding.ASCII.GetBytes("INAM"));

出力時は、save直前にShift-JISでバイト列化したByteVectorを当該項目にセットします。

    ByteVector ToByteVector(ByteVector id, String str)
    {
        return new ByteVector(SjisEnc.GetBytes(str+ " "));
    }

なぜか文字化けるので、strにスペースを一文字足してからバイト列化しています。
(スペースではなくnull('\0')が正しいのかも?)

StringをByteVectorにしたものを、次の要領でtagにセットします。

    ((TagLib.Riff.InfoTag)tag).SetValue(id, ToByteVector(id, str));

uintのTrackなど、String以外の項目はTagLibのAPIで直接操作できます。

Windows内だけ利用する場合は、これでOKでしょう。他のOS環境や、アプリによっては文字化けるかもしれません。

[追記]

Wavのヘッダー仕様はRIFFで定義されていますが、これのTRACKのIDが曖昧です
IPRTITRK, TRCKの3つが使われる場合があるようです。
TagLibは
IPRTで読み書きしていますが、WindowsのエクスプローラはITRKの値を表示します。

TagLibでITRKに値のRead/Writeは次のようにします。 

  ByteVector idTrack = new ByteVector(Encoding.ASCII.GetBytes("ITRK"));
  //Read
  uint trackNum = ((TagLib.Riff.InfoTag)tag).GetValueAsUInt(idTrack);
  //Write
  ((TagLib.Riff.InfoTag)tag).SetValue(idTracktrackNum);

-------

ついでながら、TagLibは様々なフォーマットに対応しています。
wma、m4a(aac)を試したところ、これらはutf8で問題ないようで、基本的にmp3と同じ要領で読み書きできます。

TagFile.GetTag(TagLib.TagTypes)の引数に使う定数は次のようになります。

    wma: TagLib.TagTypes.Asf
    m4a: TagLib.TagTypes.Apple

2019年6月11日火曜日

C# TagLib#のMP3タグ文字化け対策

C#でMP3タグの編集を行うアプリを作るのにTagLib-sharpを使ってみました。
まずは、こんな簡単にタグを設定できます、といった例が見つかります。

    TagLib.File tagFile = TagLib.File.Create(filePath);
    tagFile.Tag.Title = "My Favorite Things";
    tagFile.Tag.Album = "BEST";
    tagFile.Tag.Performers = new string[] { "My Favorite Singer" };
    tagFile.Save();

確かにこれでできるのですが、日本語だとWindows 10のファイルエクスプローラで文字化けすることがあります。そんなときもGrooveミュージックやiTunesでは表示されるので、これはTagLibの問題ではなく、タグを解釈するアプリ側の問題です。

手っ取り早い解決策は、次の例のようにタグのバージョンをID3v2.4にすることです。

    tag = (TagLib.Id3v2.Tag)TagFile.GetTag(TagLib.TagTypes.Id3v2, true);
    tag.Version = 4;

File作成時はMP3ファイルのタグバージョンが適用されるので、読み込み後に変更します。
GetTag()の第二引数にtrueをつけると、Tagがない場合は新規作成します。

いささか解せないのは、タグ未設定のMP3にエクスプローラのプロパティーで属性を設定すると、ID3V2.3のタグが設定されることです。ID3V2.3がデフォルトバージョンならちゃんと対応してほしいものですが...

もうひとつの方法はいったんID3V2.3を全て削除し、必要なタグだけ設定する方法です。これは後述しますが、ID3V2タグを再作成すると非同期化がOFFになるからです。

    TagFile = TagLib.File.Create(filePath);
    TagFile.RemoveTags(TagLib.TagTypes.Id3v2);
    TagLib.Id3v2.Tag tag =
        (TagLib.Id3v2.Tag)TagFile.GetTag(TagLib.TagTypes.Id3v2, true);

余分なタグを消してファイルサイズを小さくできるので、場合によっては有用でしょう。

これで一件落着なのですが、なぜ文字化けが発生するか調べたので書いておきます。

冒頭のコードで発生するエクスプローラの文字化けには、次の要因が絡んでいます。
①ID3v2タグの非同期化フラグがONになっている。
②項目により文字化ける場合と無効(非表示)となる場合がある。
③ID3V2タグで未設定だがID3V1に対応する項目があると、ID3V1の項目が適用される。
④ID3V2タグが無効だがID3V1に対応する項目があると、変更は無視され、ID3V1の項目が表示される。

①の非同期化とは、MP3v2タグに対応していなアプリがタグを音声データと誤認しないようにするための処理で、この結果UTF16文字列の先頭BOMが0xFFEE"だと”0xFF00EE"に変換されます。この場合にWindows 10のエクスプローラで文字化けが発生します。試しにバイナリエディタで”0xFFEE00"に変更してみると、文字化けが解消します。

冒頭のコードのように tagFile.TagのプロパティーにStringをセットした場合は、ID3V2.3ではUTF16ですが、ID3V2.4ではUTF8で出力されます。そのため、ID3V2.4では”0xFF00EE"は発生しません。

非同期化フラグはID3V2.4にも存在し、ONの場合に類似の処理が行われます。以下のコードでUTF16で出力すると”0xFF00EE"というバイトシーケンスが発生します。ですが、エクスプローラはID3V2.4は適切に処理してくれるようで、文字化けしません。

frameの文字コードを設定し、frameのTextにStringをセットします。

    TextInformationFrame fTIT2 =
        TextInformationFrame.Get(tag, FrameType.TIT2, StringType.UTF16, true);
    fTIT2.TextEncoding = StringType.UTF16;
    fTIT2.Text = new String[] { "タイトル” };

非同期化をOFFにすれば文字化けしなくなるのですが、MP3ファイルの非同期化フラグはTagLib.Tagクラスのプライベート変数にセットされ、それを操作するパブリックメソッドはありません。

ただし冒頭に書いたように、いったんID3V2タグを削除し再作成すれば、非同期化がOFFの状態になります。

TagLibのソースコードを変更すればプログラムでの対応も可能で、例えば次のようにTagLib.Id3v2.Tag.cs にメソッドを追加し、Save()を呼ぶ前にこのメソッドを呼ぶと非同期化をOFFにできます。

    public void ClearUnsynchronisationFlag ()
    {
        header.Flags &= ~HeaderFlags.Unsynchronisation;
    }

非同期化はID3V2.3を認識しない再生アプリのための処理なので、そんな古いアプリを考慮する必要がなければ問題ないでしょう。Windows 10のエクスプローラでも、MP3ファイルのタグ情報をプロパティダイアログの詳細で変更、保存すると非同期化はOFFになります。これを利用すればID3V2.3のままでの文字化け対策にもなります。

項目によって現象が異なり、TitleとAlbumは次の要領でUTF16BEを適用することで文字化けを解消できます。UTF16BEにはBOMがなく”0xFF00EE"が発生しないことが影響しているのでしょう。

    TextInformationFrame fTIT2 = TextInformationFrame.Get(tag2, FrameType.TIT2, StringType.UTF16BE, true);
    TextInformationFrame fTALB = TextInformationFrame.Get(tag2, FrameType.TALB, StringType.UTF16BE, true);
    fTIT2.TextEncoding = StringType.UTF16BE;
    fTALB.TextEncoding = StringType.UTF16BE;
    fTIT2.Text = new String[] { TitleString };
    fTALB.Text = new String[] { AlbumString };

既存のFrameがある場合、Getが返すFrameのStringTypeは既存の設定のままなので、StringTypeの再設定を行っています。

残念ながらPerformers(参加アーティスト)はこれでも文字化けます。あまり現実的ではありませんが、Performersは半角英数字(Laten1)に限れば次のコードで文字化け回避できます。

    TextInformationFrame fTPE1 = TextInformationFrame.Get(tag2, FrameType.TPE1, StringType.Latin1, true);
    fTPE1.TextEncoding = StringType.Latin1;
    fTPE1.Text = new String[] { PerformersString };

③の現象は、MP3ファイルにまだID3V2タグがない、あるいは対象項目がセットされていない場合に発生します。
TextInformationFrame.Get(tag2, FrameType.TIT2, StringType.UTF16, true);を使用することでID3V2タグがまだない場合は新規作成してくれます。
 TextInformationFrame.Get(tag2, FrameType.TIT2, StringType.Latin1, true);を使用することで、対象項目がセットされていない場合は追加してくれます。

それでも非同期化がONだと④の現象が発生し、ID3V2への変更は無視され、ID3V1の情報が表示されます。

ついでながら、TagLibではUTF16LEも定義されています。TagLibとしては処理しているのですが、UTF16LEではエクスプローラに文字が表示されません。GrooveミュージックでもiTunesでもダメです。調べた範囲では、ID3V2の仕様ではUTF16LEは定義されていないようです。

----------

【追記】
既に設定済みのMP3タグをTagLibで読み込むと文字化けしてしまうことがあります。
次のページに対応策が紹介されています。

2019年1月7日月曜日

Windowsキーを無効にする方法

幼児用のお遊びアプリを作ってみたのですが、なにせメチャクチャにキーをたたくので、Windowsキーなどシステムが優先的にイベントを扱うキーが押されるとスタートメニューが表示されたり、Print Scrで画面がキャプチャーされたりしてしまいます。

そこで、こうしたキーを無効にする方法を調べてみました。

まずはMicorosoftのページ。
Disabling Shortcut Keys in Games

ゲームなどで誤って Windows key を押してしまっても、好ましくない動作をしないようにする方法を紹介しています。しかし、C++なので C# のプロジェクトにはそのままでは導入できません。

こちらからはC#のサンプルがダウンロードできます。
A Simple C# Global Low Level Keyboard Hook (StomySpike / Code Project)

そのままソースを取り込んでも動作可能ですが、このソースのままだと次のようなエラーが発生します。

マネージ デバッグ アシスタント 'CallbackOnCollectedDelegate'  が発生しました
…コールバックが、型 '...+keyboardHookProc::Invoke' のガベージ コレクションされたデリゲートで行われました。…デリゲートをアンマネージ コードに渡すとき、デリゲートは 2 度と呼び出されないことが確実になるまでマネージ アプリケーションによって維持されなければなりません。'

これの解決策が次の Stack Overflow のQ&Aにありました。私は、これに従って若干変更して使っています。

C++のアンマネージドの環境ではdelegateがガベージ コレクションされなかったが、C#のマネージドの環境ではガベージ コレクションされてしまい、その結果上記のエラーが発生します。

同様の内容のページがあります。
CallbackOnCollectedDelegate が発生しました。(Cafe's Room)

callbackOnCollectedDelegateについてのMicrosoftの解説です。
callbackOnCollectedDelegate MDA

このソースで使われている関数については、次のページの説明が参考になります。
EternalWindows / Windows 開発 / メッセージ管理 / メッセージフック

2018年12月2日日曜日

C# Properties.Settings user.configの破損による例外発生への対応

Properties.Settingsを使用したアプリケーション設定を利用していますが、これが保存されるuser.configが何故か壊れ、次の例外が発生するようになりました。

 System.Configuration.ConfigurationErrorsException
 構成システムを初期化できませんでした。

たぶんデバッグ中にuser.configが正常に保存されなかったためだと思いますが、この状態になるとなんとも困ったことになります。

user.configはxmlファイルですが、これが破損するとロード時にエラーが発生します。その結果、Properties.Settings.Defaultのプロパティーに適切な値が設定されず、get/setいずれでもエラーが発生するようになってしまいます。

いろいろ試したところでは、Properties.Settings.Defaultオブジェクトは一度だけ作られ、Reloadしても作り直されません。私の結論としては、user.configを削除しアプリを再起動するしか回復の方法がありません。

user.configを削除してアプリを再起動すれば、設定は初期状態になるものの、例外は発生しなくなります。

user.configは通常は次の場所にあるでしょう。
C:\Users\UserName\AppData\Local\AppName\AppName.exe_Url_HashValue

このフォルダの場所はOSバージョンやインストールの仕方で異なる可能性があり、ハッシュ値はアプリのバージョンにより異なること可能性があります。また、開発環境ではビルドによっても異なるハッシュ値が作られることがあります。

この問題への対処方法が次のWebページにありました。

Properties.Settings.Defaultへのアクセスで例外が発生したときに、そのInnerExceptionのFileNameを使い、user.configを削除しています。

このWebページでは削除後に Settings.Default.Reload(); を呼んでいますが、どうやらReloadはすでに読み込んだxmlから値を元に戻すようで、Settings.Defaultのプロパティーのエラー状態は回復しません。Reloadではうまくいかないことはこのページのコメントにも書かれています。

そこで、次のような要領で、エラーが発生したらuser.configを削除し、アプリを再起動するようにしました。

try
{
String val = Properties.Settings.Default.someStringProperty;
}
catch (Exception ex)
{
 //MessageBox.Show("設定ファイルが破損しています。...");
string filePath = ((ConfigurationErrorsException)ex.InnerException).Filename;
File.Delete(filePath);
 Application.Restart();
}

VisualStudioでConfigurationErrorsExceptionが見つからない場合は、プロジェクトの参照にSystem.Configurationを追加します。

2018年5月20日日曜日

Windowsファイル履歴のエラー「ファイルのバックアップを実行できません」

Windows ファイル履歴実行されない場合の対処法
(同一フォルダ内の全角/半角同名のファイルをチェックするJSスクリプト)

しばらく前からWindows 10のファイル履歴が更新されなくなっていました。
イベントログを見るとエラーが発生しています。

コントロールパネル > ファイル履歴 > 詳細設定 > ファイル履歴イベントログにつぎのエラーが書かれていました。

C:\Users\(userName)\AppData\Local\Microsoft\Windows\FileHistory\Configuration\Config でユーザー ライブラリの変更のスキャンと変更されたファイルのバックアップを実行できません

これは、同じフォルダーに、全角と半角で同じ名前のファイルが保存されていると、正しくバックアップできないために発生するとのこと。

マイクロソフト コミュニテー
ファイル履歴が動作しない 

富士通Q&A
[Windows 10] ファイル履歴で正しくバックアップされているかどうかを確認する方法を教えてください。

私の場合にも
確かに上記のファイル名に該当するものがありましたが、それを直してもまだエラーが発生しました。そこで次の方法を試したところ回復しました。ライブラリに含まれるドキュメントなどの場所を変更したりしていたので、何かConfigがおかしくなったのかもしれません。
  • 次のフォルダを削除(リネームし成功したら削除する方が安全)
    C:\Users\(userName)\AppData\Local\Microsoft\Windows\FileHistory
  • コントロールパネル > ファイル履歴でドライブ選択し「今すぐ実行」
問題となるファイルを探すのに目視で調べるのは厄介なので、javascriptでファイル名のダブりをチェックするスクリプトを作ってみました。このスクリプトで”要チェック”と表示されるファイル/フォルダ名を調べてみてください。エラーにならない場合もどちらか一方しかファイル履歴に保存されていない可能性がありますので、チェックしてみる価値はあるでしょう。

このブログの後半にソースを載せてあります。

このスクリプトには全角/半角文字の対象リストがあります。私の場合はこれで問題になっていたファイルが見つけられましたが、このリストに問題になる文字が全て含まれているかわかりません。また、このスクリプトで”要チェック”とされても、実際には問題ない場合もあるでしょう。

状況はいささか複雑で、ファイル履歴が更新されるがいずれか一方しか履歴に保存されない場合もあり、またファイル履歴に保存・更新される順序でエラーになったりならなかったりすることもあるようです(手間がかかるので十分調べていません)。
また、フォルダ名とファイル名の間でも同様のことが発生します。

以下は次の環境で試した結果です。Windowsのバージョンによって動作が異なるかもしれません。

テスト環境:Window 10 Pro, バージョン 1709, OS ビルド 26299.431
  • エラーが発生する例:
    space .txt <=> space .txt  (全角/半角スペース)
    space ].txt <=> space ].txt  (全角/半角スペース)
    TEST1.txt <=> TEST1.txt (全角/半角数字)
    test  <=> TEST (半角フォルダ名と全角ファイル名)  
  • エラーは発生しないがどちらか一方しか履歴に残らない例:
    space[ ].txt <=> space[ ].txt  (全角/半角スペース)
    TEST.TXT <=> test.txt  (全角/半角アルファベット、ピリオドいずれも半角)
    test#  <=> TEST# (半角フォルダ名と全角ファイル名)
なぜこんなことが起こるか考えてみました。

ポイントは、エラーは発生しないがどちらか一方しか履歴に残らない場合がある、ということでしょう。エクスプローラにとっては別ファイルだが、ファイル履歴にとっては同一ファイルとみなされる場合あるということです。

まずはWindowsのファイル名ではアルファベットの大文字小文字は区別されません。TEST.TXTとtest.txtは同じフォルダ内では同一ファイルとなり、そもそも別々に保存できません。これはファイル履歴でも同じです。

ファイルエクスプローラでのファイル名順のソートはSQLサーバの照合順序(WI)に準じているようで、全角/半角で同一文字とみなされるものは同等に扱われます。全角Aと半角Aは同等ですがイコールではなく、いずれも全角B、半角Bより小さいが、全角Aは半角Aより大きいと判断され、全角/半角はファイル名としては別のものとして扱われます。TEST.TXTとTEST.TXTは別ファイルになります。ところが、ファイル履歴ではどちらか一方しか保存されません。つまり同一ファイル(イコール)とみなされます。

ファイル履歴ではファイルエクスプローラの照合順序で同等のファイル名は同一ファイルとして扱われているような感じです。照合順序の判定では最初に同一文字として扱う文字を半角大文字に統一するなどして大小比較するでしょう。ここでイコールの場合に、ファイルエクスプローラでは元の名前で比較し大小を決定しているようですが、ファイル履歴では同一ファイルとして扱っているのではないかと思います。どういう場合にエラーになるかはわかりませんが、異なるファイルを同一ファイルとして扱おうとするのだから、エラーが発生しても不思議ではありません。

理由はともあれ、あやしそうなファイルはリネームしておいた方が無難でしょう。
------------------- スクリプト -------------------

//以下のスクリプトをテキストファイルにコピーし、
//fileNameCheck.jsなどの拡張子.jsの名前で保存する。

//コマンドプロンプト(cmd.exe)で、CScriptで実行する。
//例: >CScript fileNameCheck.js

//WScriptだとEchoの度にダイアログが表示される。

//ファイル履歴の対象フォルダをプログラムで取得する方法がわからないので、
//チェックするフォルダのリストを各自の環境に合わせて設定する。
var homeDir = "C:\\Users\\UserName\\";
var folderPaths = [
    homeDir + "Desktop",
    homeDir + "Documents",
    homeDir + "Pictures",
    homeDir + "Videos",
    homeDir + "Music"
];

//半角と同じと判断される文字の全角、半角対照辞書。
//regularizeNameでzen2hanに含まれる全角文字が見つかった場合、半角文字に置き換える。
//'#'などの記号は問題ないかもしれない。
//また、ハイフン、ピリオドなどは対応する全角文字が複数あるため、適切な対応は不明。
var zen2han = {
    ' ': ' ',
    'a': 'a',
    'b': 'b',
    'c': 'c',
    'd': 'd',
    'e': 'e',
    'f': 'f',
    'g': 'g',
    'h': 'h',
    'i': 'i',
    'j': 'j',
    'k': 'k',
    'l': 'l',
    'm': 'm',
    'n': 'n',
    'o': 'o',
    'p': 'p',
    'q': 'q',
    'r': 'r',
    's': 's',
    't': 't',
    'u': 'u',
    'v': 'v',
    'w': 'w',
    'x': 'x',
    'y': 'y',
    'z': 'z',
    'A': 'a',
    'B': 'b',
    'C': 'c',
    'D': 'd',
    'E': 'e',
    'F': 'f',
    'G': 'g',
    'H': 'h',
    'I': 'i',
    'J': 'j',
    'K': 'k',
    'L': 'l',
    'M': 'm',
    'N': 'n',
    'O': 'o',
    'P': 'p',
    'Q': 'q',
    'R': 'r',
    'S': 's',
    'T': 't',
    'U': 'u',
    'V': 'v',
    'W': 'w',
    'X': 'x',
    'Y': 'y',
    'Z': 'z',
    '1': '1',
    '2': '2',
    '3': '3',
    '4': '4',
    '5': '5',
    '6': '6',
    '7': '7',
    '8': '8',
    '9': '9',
    '0': '0',
    '@': '@',
    '!': '!',
    '"': '"',
    '#': '#',
    '$': '$',
    '%': '%',
    '&': '&',
    '\': '\'',
    '(': '(',
    ')': ')',
    '=': '=',
    '-': '-',
    '~': '~',
    '^': '^',
    '|': '|',
    '\': '\\',
    '[': '[',
    ';': ';',
    ':': ':',
    ']': ']',
    ',': ',',
    '.': '.',
    '/': '/',
    '`': '`',
    '{': '{',
    '+': '+',
    '*': '*',
    '}': '}',
    '<': '<',
    '>': '>',
    '?': '?',
    '_': '_'
};

var fso = new ActiveXObject('Scripting.FileSystemObject');
//zenに含まれる文字が対象文字列にあるか判断するのに用いる正規表現。初期化時にセットする。
var regexZen;

//.jsファイルをダブルクリックするとWScriptで実行されるので、exeの名前をチェックし、WScriptの場合は警告を出して終了する。
if (/cscript\.exe$/i.test(WScript.FullName)) {
    //zenに含まれる文字が対象文字列にあるか判断するのに用いる正規表現をセットする。
    var str = "[";
    for (key in zen2han) {
        str += key;
    }
    str += "]";
    regexZen = new RegExp(str);
    //対象のフォルダを順次チェックする。
    for (var i = 0; i < folderPaths.length; i++) {
        var folderPath = folderPaths[i];
        if (fso.FolderExists(folderPath)) {
            folderCheck(fso.GetFolder(folderPath));
        } else {
            WScript.Echo("Folderなし: " + folderPath);
        }
    }
} else {
    WScript.Echo("CScriptで実行してください。")
}

function folderCheck(folder) {
    //動作確認時にコメントを外すとチェック中のフォルダ名をログ表示する。
    //WScript.Echo("FOLDER: " + folder.Path);

    //フォルダ毎に辞書を初期化。
    var nameDict = [];
    //フォルダ内のファイル名をチェック。
var files = new Enumerator(folder.Files);
    for (; !files.atEnd(); files.moveNext()) {
        var file = files.item();
        regularizeName(file, nameDict);
    }
    //サブフォルダの名前をチェックし、再帰的にfolderCheckを実行。
var subFolders = new Enumerator(folder.SubFolders);
    for (; !subFolders.atEnd(); subFolders.moveNext()) {
        var subFolder = subFolders.item();
        regularizeName(subFolder, nameDict);
folderCheck(subFolder);
}
}

//zenに含まれる文字がある場合、それに対応するhanの文字に置き換える。
function regularizeName(item, nameDict) {
    var name = item.Name, newName;
    if (regexZen.test(name)) {
        //zen2hanに含まれる全角文字が見つかった場合、対応する半角に置き換え。
        var chars = name.split('');
        for (var n = 0; n < chars.length; n++) {
            var han = zen2han[chars[n]];
            if (han) {
                chars[n] = han;
            }
        }
        newName = chars.join('').toLowerCase();
    } else {
        //zen2hanに含まれる全角文字がない場合は小文字に統一。
        newName = name.toLowerCase();
    }
    var registerdItem = nameDict[newName];
    if (registerdItem) {
        //zen2hanで全角を半角に変換した名前が既に登録済みの場合にログを出力。
        WScript.Echo("要チェック:" + item.Path + " <=> " + registerdItem.Name);
    } else {
        //まだnameDictに登録されていない場合は追加。
        nameDict[newName] = item;
    }
}

    2017年11月15日水曜日

    WiX toolset msi ダイアログ多国語対応 

    以前のブログでWiX toolsetによる多国語対応msiの作り方について書いたが、インストーラの主要なメッセージの表示が日本語化されても、細かなダイアログのメッセージが英語のままであった。

    WiX tooksetの以下の場所に多国語メッセージ定義ファイルがある。
    C:\Program Files (x86)\WiX Toolset v3.11\SDK\wixui

    これに含まれる WixUI_ja-jp.wxl で日本語メッセージが設定されているが、これが適用されない項目がある。

    ここに同様の件でのQ&Aがあった。
    WIX: Statuses while rolling back uninstall are not localized in French

    これによるとActionに対応するローカライズテキストがデフォルトでは設定されていないようで、Product.wxsに次のような追加を行うと、日本語メッセージが適用されるようになる。

    この Q&A では String の Id を別のものに置き換え、言語別 wxl でメッセージを書き換えているが、WiX規定のものであれば次の要領で日本語を適用できる。

     <UI>
        <ProgressText Action="RemoveShortcuts" Template="!(loc.ProgressTextRemoveShortcutsTemplate)">
                !(loc.ProgressTextRemoveShortcuts)
        </ProgressText>
    </UI>
    

    
    
    これにより Action="RemoveShortcuts” に ja-JP では WixUI_ja-jp.wxl の ProgressTextRemoveShortcuts が適用されるようになる。

     WixUI_ja-jp.wxl では次のように設定されている。

    <String Id="ProgressTextRemoveShortcuts" Overridable="yes">ショートカットを削除しています</String>
    <String Id="ProgressTextRemoveShortcutsTemplate" Overridable="yes">ショートカット: [1]</String>
    

    ProgressText の Template がなくてもエラーにはならないが、メッセージが表示されなくなるかもしれないので設定しておいたほうがよいだろう。ただし、メッセージによっては Template が設定されいないものがあり、その場合は Template は不要で、設定したが対応するStringがないとエラーになる。

    Actionに対応するものは以上で置き換わるが、これだけでは日本語化されないものもある。
    たとえば、”Please wait while Windows configures MyApp"があるが、これはWiX toolset で次のように設定されている。



    ErrorProgressText.wxs
        <Error Id="20">!(loc.Error20)</Error>
    
    WixUI_en-us.wxl
        <String Id="Error20" Overridable="yes">Please wait while Windows configures [ProductName]</String>
    

    このようなものについては Product.wxsの UI タグに次のように設定を追加する。
    
    
     <UI>
        <Error Id="20">!(loc.Error20)</Error>
    </UI>

    以下 UI タグの設定例。気付いたところだけ対応したもの。また、Actionについては上記Q&Aに含まれていたものを入れてあるので、msiによっては不要なものもある。

    <UI>
        <ProgressText Action="FileCost">!(loc.ProgressTextFileCost)</ProgressText>
        <ProgressText Action="InstallFiles" Template="!(loc.ProgressTextInstallFilesTemplate)">!(loc.ProgressTextInstallFiles)</ProgressText>            
        <ProgressText Action="CreateShortcuts" Template="!(loc.ProgressTextCreateShortcutsTemplate)">!(loc.ProgressTextCreateShortcuts)</ProgressText>
        <ProgressText Action="WriteRegistryValues" Template="!(loc.ProgressTextWriteRegistryValuesTemplate)">!(loc.ProgressTextWriteRegistryValues)</ProgressText>
        <ProgressText Action="RegisterUser" Template="!(loc.ProgressTextRegisterUserTemplate)">!(loc.ProgressTextRegisterUser)</ProgressText>
        <ProgressText Action="RegisterProduct" Template="!(loc.ProgressTextRegisterProductTemplate)">!(loc.ProgressTextRegisterProduct)</ProgressText>
        <ProgressText Action="PublishFeatures" Template="!(loc.ProgressTextPublishFeaturesTemplate)">!(loc.ProgressTextPublishFeatures)</ProgressText>
        <ProgressText Action="PublishProduct">!(loc.ProgressTextPublishProduct)</ProgressText>
        <ProgressText Action="RemoveFiles" Template="!(loc.ProgressTextRemoveFilesTemplate)">!(loc.ProgressTextRemoveFiles)</ProgressText>
        <ProgressText Action="RemoveExistingProducts" Template="!(loc.ProgressTextRemoveExistingProductsTemplate)">!(loc.ProgressTextRemoveExistingProducts)</ProgressText>
        <ProgressText Action="RemoveShortcuts" Template="!(loc.ProgressTextRemoveShortcutsTemplate)">!(loc.ProgressTextRemoveShortcuts)</ProgressText>
        <ProgressText Action="Rollback" Template="!(loc.ProgressTextRollbackTemplate)">!(loc.ProgressTextRollback)</ProgressText>
        <ProgressText Action="RollbackCleanup" Template="!(loc.ProgressTextRollbackCleanupTemplate)">!(loc.ProgressTextRollbackCleanup)</ProgressText>
        <Error Id="16">!(loc.Error16)</Error>
        <Error Id="20">!(loc.Error20)</Error>
        <Error Id="21">!(loc.Error21)</Error>
    </UI>

    でも、これって ErrorProgressText.wxs の一部ですね。ということで、ErrorProgressText.wxs の UI タグの中身をすっかり Product.wxs にコピーしてみた。特に問題はなさそうだ。違いはというと ErrorProgressText.wxs は wixlib のビルド時に適用され、Product.wxs はmsiビルド時に適用されているということだと思う。

    関連ブログ

    2017年11月12日日曜日

    WiX toolsetのBootstarpperを試してみた(4)UI追加

    さて、前回まででmsi側のUIを使うBootstapperが一応できたが、アンインストール時になにも表示せずに実行、終了してしまうので、UIの追加を行ってみる。

    次のようなコードで、OnDetectPackageCompleteで実行/キャンセルの確認ダイアログを表示し、OnApplyCompleteで終了メッセージが表示される。

    参照に System.WIndows.Forms を追加。
    
    using System.Windows.Forms;
    
    private void OnDetectPackageComplete(object sender, DetectPackageCompleteEventArgs e)
    {
        if (e.PackageId == "MyAppSetup.msi")
        {
            if (e.State == PackageState.Absent)
            {
                Engine.Plan(LaunchAction.Install);
            }
            else if (e.State == PackageState.Present)
            {
                DialogResult res = MessageBox.Show("MyAppSetup",
                    "アンインストールを実行しますか?", MessageBoxButtons.OKCancel);
                if (res == DialogResult.Cancel)
                {
                    Engine.Quit(0);
                }
                else
                {
                    Engine.Plan(LaunchAction.Uninstall);
                }
            }
        }
    }
    
    private void OnApplyComplete(object sender, ApplyCompleteEventArgs e)
    {
        if (IsUninstall)
        {
            MessageBox.Show(ClassLibrary1.Properties.Resources.UnistallDone);
        }
        Engine.Quit(0);
    }
    
    
    せっかくmsiを多国語化したのに、Bootstarpperが未対応になってしまった。しかし、BootstarpperはC#のクラスライブラリなので、プロジェクトのProperties に String の Resouces.resx を追加することで多国語対応できる。

    msiと同様、ベースを英語とし日本語のResources.ja.resxを追加した場合は次のようにBundle.wxs の BootstrapperApplicationRef にresources.dllを追加する。

    <BootstrapperApplicationRef Id='ManagedBootstrapperApplicationHost'>
        <Payload SourceFile="..\ClassLibrary1\bin\Release\ClassLibrary1.dll" />
        <Payload SourceFile="..\ClassLibrary1\bin\Release\ja\ClassLibrary1.resources.dll"
            Name="ja\ClassLibrary1.resources.dll" />
        <Payload SourceFile="BootstrapperCore.config" />
    </BootstrapperApplicationRef>
    

    Name属性で ja サブフォルダを追加している。Name属性は展開先のパスで、デフォルト値はファイル名であるため、これがないと同じフォルダにファイルがフラットに展開され、resources.dllがロードされずベース言語のみとなる。

    ここまでで、Cultureに従ってパラメータを切り替えて多国語化した msi を起動するBootstrapper作るという目的は一応実現した。しかし、これだけのためならdotNetInstallerを使ったBootstrapperのほうが目的に適っている。BurnをするならUIをBootstrapperに移し、msiはサイレントで実行した方がよい。

    上記の方法ではインストール実行中、常時ウインドウを表示しておくことはできない。Runは独立したスレッドで呼ばれ、Runからリターンするとスレッドも終了し、ここで表示したFormも消えるか、下手をするとゾンビで残る。また Engine.Quit(0) を呼ぶとプロセスが終了しUIも終了してしまうので、UIが終了してから Engine.Quit(0) を呼ぶ必要がある。

    そのためにはUI用のイベントループを作ればよい。ここではFormアプリによる実装例を紹介しておく。

    Run メソッドで Detect を呼んだあと、Application によるループを開始する。
    このループ終了後、Engine.Quit(0) を呼ぶ。
    BootstapperとFormで相互に互いのインスタンスの参照を持ち、適宜メソッド呼び出し、変数設定などを行う。

    /クラスライブラリにForm1を追加
    private Form1 form;
    
    protected override void Run()
    {
        this.DetectPackageComplete += this.OnDetectPackageComplete;
        this.PlanComplete += this.OnPlanComplete;
        this.ApplyComplete += this.OnApplyComplete;
        this.ExecuteProgress += this.OnExecuteProgress;
    
        if (CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "ja")
        {
            Engine.StringVariables["TRANSFORMS"] = ":ja-JP.mst";
        }
        else
        {
            Engine.StringVariables["TRANSFORMS"] = "";
        }
        Engine.Detect();
    
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        form = new Form1();
        form.Bootstrapper = this;
        Application.Run(form);
    
        //formがクローズされると Application.Run が終了し、ここにくる。
        this.Engine.Quit(0);
    }
    

    また、ExecuteProgressイベントで進行状態が通知される。ここでFormに通知を転送することができ、また ExecuteProgressEventArgs にResult.Cancelをセットするとインストールを中断することができる。

    private void OnExecuteProgress(object sender, ExecuteProgressEventArgs e)
    {
        if ( someEroorCoccured )
        {
            e.Result = Result.Cancel;
        }
        else
        {
            form.SetProgress(e.OverallPercentage.ToString());
        }
    }
    

    関連ブログ
    VisualStudio 2017 Community & WiX tools & WiX Edit によるWindowsアプリ インストーラ(msi)作成
    WiX toolset msi ダイアログ多国語対応
    dotNetInstallerによるシンプルBootstarpper作成
    WiX toolsetのBootstarpperを試してみた(1)
    WiX toolsetのBootstarpperを試してみた(2)Detect追加
    WiX toolsetのBootstarpperを試してみた(3)msiexecパラメータ設定

    2017年11月10日金曜日

    WiX toolsetのBootstarpperを試してみた(3)msiexecパラメータ設定

    前回で Bundle から msi の UI を使ってのインストール/アンインストールができるようになった。今回は Bundle のブートストラッパ―で設定したパラメータを用いて msi を起動し、msi側で言語切り替えを行うようにする。
    • Bundle の子要素にVariablesタグ追加
      <Bundle ...>
      <Variable Name="TRANSFORMS" 
           Value=":ja-JP.mst" Type="string" bal:Overridable="yes"/>
      • Bundleエンジンに変数を追加する。
        Name:変数名。
        Value:初期値。この例では必ずBootstarpperで設定するので、Valueは別の値でもよい。
      • bal:Overridable="yes": Bootstarpperで変更できるように"yes"に設定する。
        • bal namespace が認識されない場合はWixタグに次のxmlnsを追加
          xmlns:bal="http://schemas.microsoft.com/wix/BalExtension"
          
          
    • MsiPackage 要素変更
    • <Chain>
          <MsiPackage ....>
              <MsiProperty Name="TRANSFORMS" Value="[TRANSFORMS]" />
          </MsiPackage>
      </Chain>
      
      
      • Name:msi 作成プロジェクトの Product.wxsのPropertyタグのIdと同じ名前。
        msiexecの起動パラメータとして使われる。
        <Property Id="TRANSFORMS" Value="Default" />
        
      • Value="[TRANSFORMS]":BundleのVariableのTRANSFORMSの値で置換される。
        
        
    • MyBootstrapper.csのRunメソッド変更
      次のようなコードで Variable TRANSFORMS の値をセットする。
      if (CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "ja")
      {
          this.Engine.StringVariables["TRANSFORMS"] = ":ja-JP.mst";
      }
      else
      {
          this.Engine.StringVariables["TRANSFORMS"] = "";
      }
      
      
    • MySetup.exe実行これらの設定、コードの変更後、MySetup.exeを実行するとMyAppSetup.mstを起動するmsiexecのパラメータに
      TRANSFORMS=":ja-JP.mst"、またはTRANSFORMS=""が追加される。
    • ログ出力
      Bundleのログに、次のように書き出される
      • Installの場合:
        Applying execute package: MySetup.msi, action: Install, 
            path: C:\Users\UserName\AppData\Local\Package Cache\{GUID}v1.0.1\MyAppSetup.msi, 
            arguments: ' ARPSYSTEMCOMPONENT="1" MSIFASTINSTALL="7" 
            TRANSFORMS=":ja-JP.mst"
        
        
      • Uninstallの場合:
        Applying execute package: MySetup.msi, action: Uninstall, 
            path: (null), 
            arguments: ' ARPSYSTEMCOMPONENT="1" MSIFASTINSTALL="7" 
            TRANSFORMS=":ja-JP.mst"
        
        
      • Valueが空文字の場合は TRANSFORMS="" となる。
    これでmsiのUIを使ったインストールは実現できたが、アンインストールはサイレントで実行される。

    TRANSFORMSと同様にmsiexecのパラメータ UILevel、CLIENTUILEVEL を設定してみたが、Bundleが CLIENTUILEVEL=3 (サイレント)をその後ろに付けるため有効にならない。アンインストール実行確認ダイアログや、アンインストール完了時のメッセージ表示などのUIはBootstarpperで用意する必要がある。
    • アンインストール時のmsiexecのログ
      ARPSYSTEMCOMPONENT=1 MSIFASTINSTALL=7 TRANSFORMS=:ja-JP.mst 
          UILevel=5 CLIENTUILEVEL=5 REBOOT=ReallySuppress IGNOREDEPENDENCIES=ALL 
          REMOVE=ALL CURRENTDIRECTORY=C:\WINDOWS\system32 
          CLIENTUILEVEL=3 MSICLIENTUSESEXTERNALUI=1 CLIENTPROCESSID=6296
      
      
      • UILevel=5 CLIENTUILEVEL=5 はMsiPropertyで設定したもの。burnが設定するCLIENTUILEVEL=3が有効になる。
    関連ブログ
    VisualStudio 2017 Community & WiX tools & WiX Edit によるWindowsアプリ インストーラ(msi)作成
    WiX toolset msi ダイアログ多国語対応
    dotNetInstallerによるシンプルBootstarpper作成
    WiX toolsetのBootstarpperを試してみた(1)
    WiX toolsetのBootstarpperを試してみた(2)Detect追加
    WiX toolsetのBootstarpperを試してみた(4)UI追加

    WiX toolsetのBootstarpperを試してみた(2)Detect追加

    前回はPlanCompleteとApplyComplete のイベントハンドラ だけを追加したクラスからインストールを開始し、msiのUIによるインストール/アンインストールを行うところまで設定した。

    今回は DetectPackageComplete を追加する。
    • Runメソッド変更
      protected override void Run()
      {
          this.DetectPackageComplete += this.OnDetectPackageComplete;
          this.PlanComplete += this.OnPlanComplete;
          this.ApplyComplete += this.OnApplyComplete;
          Engine.Detect();
      }
      
    • DetectPackageComplete イベントハンドラ追加
      private void OnDetectPackageComplete(object sender, DetectPackageCompleteEventArgs e)
      {
          if (e.PackageId == "MySetup.msi")
          {
              if (e.State == PackageState.Absent)
              {
                  Engine.Plan(LaunchAction.Install);
              }
              else if (e.State == PackageState.Present)
              {
                  Engine.Plan(LaunchAction.Uninstall);
              }
          }
      }
      
    前回は Runメソッドで Engine.Plan を呼んでいたが、これを Engine.Detect に変更。その結果、MySetup.msi が見つからなければ Install、見つかれば Unistall を実行するようになる。

    これにより MySetup.exe を複数回起動すると、Install と Unistall が交互に繰り返されるようになる。

    このとき、次のようなログが書かれる。
    Detected package: MySetup.msi, state: Absent, cached: None
    または
    Detected package: MySetup.msi, state: Present, cached: Complete

    何度実行しても常に Absent の場合は msi 作成時の ID 設定をチェックする。これが"*"の場合は Present にならない。"*"を固定の GUID に変更する。

    Engine.Detect では他に次のイベント呼び出しがあるが、詳しく調べていない。
    • DetectRelatedMsiPackage
    • DetectRelatedBundle
    • DetectComplete
    次回で Bootstapper で msi 起動時のパラメータを設定し、言語切り替えを行えるようにする。
    参考リンク
    Bootstrapper Application Interface
    IBootstrapperApplicationの説明。ソースを見ていないが、ManagedBootstrapperApplicationHost はこれを実装し、C#の BootstrapperApplication のイベント呼び出しを行うものでしょう。

    Creating a custom UI installer with WIX Burn Bootstrapper
    WPFアプリケーションにブートストラッパ実装例。

    EngineとBootstrapperの関係、イベント一覧があります。

    発生順のイベントリストがあります。


    2017年11月7日火曜日

    WiX toolsetのBootstarpperを試してみた(1)

    多国語対応のmsiのためのBootstarpper作りでWiXのBurnを試してみた。これによる言語切り替えも実現できたのだが、それだけの目的ならdotNetInstallerによるBootstrapper作りの方が面倒がない。とはいえ本格的なインストーラを作る場合には強力なツールなので、Burnについて試したことをまとめておく。

    インストール手順は次のブログを参照
    VisualStudio 2017 Community & WiX tools & WiX Edit によるWindowsアプリ インストーラ(msi)作成

    概要
    Burnは複数のプロジェクトを一つのバンドルにまとめるもので、そのプロセルは大きくふたつに別れる。インストールを開始する前に実行するBootstarpperと、それに続く一連のインストール実行だ。

    Burnを使ってインストーラを作る場合は、Bootstarpperにより必須環境を整え、そのUIによりユーザがインストール等の条件を設定し、それ以降はその条件に従い実行され、個々のmsiでは設定を変更しないのが基本のようだ。

    msiにUIを表示させることはできるが、長時間がかかるインストールの途中で何度もダイアログを表示するのはいただけないし、Bootstarpperが決めた条件をその後のmsiで変更できるようでは、複数のmsi間で不整合が生じうる。

    また、Burnはインストール時のシーケンスを作るだけでなく、アップデート、変更、アンイストールも含めて管理するできるようになっている。そのため、Burnが作るバンドルもレジストリに登録され、コントロールパネルの「プログラムと機能」一覧に表示される。個々のmsiもここに表示することは可能だが、全てバンドルで管理できるようにしておく必要がある。

    開発者のコメントもあります。
    B is for Bundle and that's good enough for me.

    それでも、BundleのBootstapperからオプションを設定してmsiを起動し、msiのUIでインストール/アンインストールを行うことができる。以下、多言語対応msi用のBundle作りを題材とした手順。

    はじめに

    試す前にレジストリのバックアップを取っておくことをお勧めする。アンインストールが適切に行なえないと、インストーラでは削除できない項目がレジストリに残ることがある。

    インストーラのログは次の場所に書き出される。
    C:\Users\UserName\AppData\Local\Temp\

    Burnによるバンドル作成
    • msiプロジェクトを含めるソリューションを開く。
    Bootstarpperプロジェクト追加
    • ソリューション右クリック > 追加 > 新しいプロジェクト
      左ペイン WiX toolset v3 > 中ペイン Bootstapper Project for WiX v3 > OK
      • Bootstrapper1の名前で保存
        これでソリューションにBootstarpperプロジェクトが追加される。
    • Bundle.wxs変更
      • namespace追加
      • <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
             xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
         
        • xmlns:balは必須ではないがやがて必要になる
      • Bundle要素を変更
        <Bundle Name="MyAppSetup" Manufacturer="name" ...
                UpgradeCode="GUID">
        
        
        • Name: コントロールパネルのプログラムの機能一覧に登録される名前。
          exeの名前はプロジェクト > プロパティー > Output nameで設定。
        • Manufacturer: 空だとエラーになる。
        • UpgradeCode: msiとは別のものを設定する。
      • Chain要素にMsiPackage要素を追加
      • <Chain>
          <MsiPackage SourceFile="..\SetupProject1\bin\Release\WavCutterSetup.msi"
                      DisplayInternalUI="yes" Visible="no">
            <MsiProperty Name="TRANSFORMS" Value="[TRANSFORMS]" />
          
          </MsiPackage>
        </Chain>
      • 
        
        • DisplayInternalUI="yes"の場合、msiのUIを表示(実体はmsiexecの起動パラメータ)
        • Visible="yes"の場合、msiもコントロールパネルの「プログラムと機能」一覧に登録される。
    StandardBootstrapperApplication.RtfLicenseによるテスト
    • BootstrapperApplicationRef要素追加
      <BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
      
      
      • StandardBootstrapperApplication: 予め用意されている標準アプリ
      • RtfLicenseはライセンス表示後Chain中のPackageを実行する。
        • HyperlinkLicenseもあるが、試していない。

    • ビルド > MyAppSetup.exe実行
      ライセンスに同意し、Installをクリックするとmsiが実行される。
      msiが再度ライセンス同意を求める。

    • 「プログラムと機能」に登録
      上記設定の場合、コントロールパネルの「プログラムと機能」一覧にMyAppSetupが登録される。

    • MyAppSetupダブルクリック
      MyAppSetupダブルクリックで変更/アンインストールが実行される。
      「Repair」「Uninstall」はBundleとしてのものなので、msiがこれらに対応していなくてもが表示される。

    • 削除実行
      「Uninstall」クリックで、サイレントでmsiによる削除が実行される。
    Bundle想定しているインストールの手順は、概ねこのようなものと考えられる。

    ここでは何も設定していないが、本来はDOT.NET Frameworkのチェック、インストールなど、必須環境を整える作業を行ってから、Chain内のインストールが順次行われる。

    ちなみに、msiでは必須コンポネントの存在有無チェックは行えるが、無い場合はメッセージ表示でインストールを中断する。

    アンインストール

    このMyAppSetupを二度実行した場合、既にBundleが登録されているため、コントロールパネルからの場合と同様に「Modify Setup」のダイアログとなる。

    msiにスタートメニューへのアンインストールボタン追加が設定されている場合は、そのボタンも追加される。先にmsiによるアンインストールを行うと、このアプリは削除されるがBundleは残っているため、再度MyAppSetupを実行すると「Modify Setup」のダイアログとなる。Bundleでアンインストールを行えば、アプリ削除後であってもBundleが削除される。msiがひとつだけだと違和感はあるが、Bundleの動作としては問題ない。

    MsiPackageでVisible="yes"とすると、msiも「プログラムと機能」一覧に追加されるので、これと同じことが起こる。

    ManagedBootstrapperApplicationHostによる機能追加
    RtfLicenseは表示するライセンス、ダイアログのスタイルなどをカスタマイズできるが、UIに機能を追加することはできない。これが必要な場合はManagedBootstrapperApplicationHostを使用する。

    ManagedBootstrapperApplicationHost は BootstrapperApplication のサブクラスを含むクラスライブラリ(.dll)を読み込み、そのBootstrapperApplication インスタンスのイベントハンドを順次呼び出すことで、インストール環境を整える。
    • クラスライブラリ追加
      ソリューション右クリック > 追加 > 新しいプロジェクト > Visual C# > クラスライブラリ(.NET Framework)
      • プロジェクト名、アッセンブリ名=ClassLibrary1で追加。
        追加するクラスはMyBootstrapper.csとする。
      • 参照追加
        C:\Program Files (x86)\WiX Toolset v3.11\SDK\BootstrapperCore.dll
      • 次のconfigファイルをBootstrapper1プロジェクトへ追加
        C:\Program Files (x86)\WiX Toolset v3.11\SDK\BootstrapperCore.config
        
        
      • BootstrapperCore.config 変更
        <host assemblyName="ClassLibrary1"></host>
    • BootstrapperApplicationRef の設定を変更
    • <BootstrapperApplicationRef Id='ManagedBootstrapperApplicationHost'>
          <Payload SourceFile="..\ClassLibrary1\bin\Release\ClassLibrary1.dll" />
          <Payload Name="BootstrapperCore.config" SourceFile="BootstrapperCore.config" />
      </BootstrapperApplicationRef>
      
      <WixVariable Id="WixMbaPrereqPackageId" Value="Netfx4Full" />
      <WixVariable Id="WixMbaPrereqLicenseUrl" Value="NetfxLicense.rtf" />
      
      
      • WixMbaPrereqPackageId、WixMbaPrereqLicenseUrl が設定されていないとエラーになる。
    • MyBootstrapper.csの実装
        • namespace行の前にアノテーション追加
          [assembly:BootstrapperApplication(typeof(MySetup.MyBootstrapper))]
          namespace MySetup
          {
              public class MyBootstrapper : BootstrapperApplication
              { .... }
          }
          
          
        • Runメソッド追加
          public class MyBootstrapper : BootstrapperApplication
          
          protected overridevoid Run()
          {
               Engine.Quit(0);
          }
          
          • Run()の中でUI表示や、インストール条件設定など行う。
          • Engine.Quit(0)でexeの終了処理に入る。ここで実行すると実際には意味がないが、これがないとバックグランドプロセスが残るのでテスト中は適当なところでEngine.Quit(0)を呼ぶ。もし呼びそこなうと、タスクマネージャーでプロセスを終了させることになる。
        • イベントハンドラ追加
          少なくとも次のふたつのイベントハンドラを追加する。
          protected override void Run()
          {
              PlanComplete += OnPlanComplete;
              ApplyComplete += OnApplyComplete;
              if (Command.Action == LaunchAction.Install)
              {
                  Engine.Plan(LaunchAction.Install);
              }
              else
              {
                  //「プログラムと機能」一覧から起動した場合
                  Engine.Plan(LaunchAction.Uninstall);
              }
              //Engine.Quit(0)はここでは呼ばない。
          }
          
          private void OnPlanComplete(object sender, PlanCompleteEventArgs e)
          {
              if (e.Status >= 0)
              {
                  Engine.Apply(System.IntPtr.Zero);
              }
              else
              {
                  Engine.Quit(0);
              }
          }
          private void OnApplyComplete(object sender, ApplyCompleteEventArgs e)
          {
              Engine.Quit(0);
          }
          
      ここまでで msi 単独のときと近い動作になるが、MyAppSetup.exe 起動時の Install / Uninstall の切り分けをコマンドラインパラメータからセットされる LaunchAction で行っているため、MyAppSetup.exe を繰り返し起動したとき常にInstallしようとし、結果二度目以降は何もせずに終了する。

      2017年11月6日月曜日

      dotNetInstallerによるシンプルBootstarpper作成

      前回で多言語対応のmsiを作成したので、コマンドラインオプションを設定してmsi起動するBootstarpper作りを試みた。

      本格的なものを作るならWiXのBurnを用いてBundleを作ることができるが、その場合はUIをBundle側のBootstrapperに移し、msiはBootstrapperの設定にしたがってUIなしで実行するのが本道のようだ。となると、せっかくmsiを多国語対応した意味がなくなるので
      、このmsiを利用するシンプルなBootstarpperを作りを試みた。

      ちなみにWiXでmsiにActionを追加することができるが、これはmsiが起動したあとで使われるので、起動時の言語切り替えには使えなかった。

      まずはmsiとは別のexeを作り、そこからWindowsのCulture設定に従いmsiを起動する方法。例えば次のようなコードでコンソールアプリを作ればよい。

      static void Main(string[] args)
      {
          FileInfo msiFileInfo = new FileInfo(Directory.GetParent(Assembly.GetExecutingAssembly().Location) + "\\MyAppSetup.msi");
          if (!CultureInfo.Exists) {
      
          if (CultureInfo.CurrentCulture.Name == "ja-JP")
          {
              System.Diagnostics.Process.Start(msiFileInfo.FullName, "TRANSFORMS=\":ja-JP.mst\"");
          }
          else
          {
              System.Diagnostics.Process.Start(msiFileInfo.FullName);
          }
      }
      

      この方法の問題点はmsiとexeの二つが存在し、msiを直接起動することもできてしまうことだ。言語切り替えだけであれば実質的な問題はないとも言えるが、できれば一本化したい。

      dotNetInstaller 

      探してみたところ dotNetInstaller というオープンソフトがあった。公式ページではV2.3(Windows8対応)までだが、GitHubからはV2.4(Window10 対応)をダウンロードできる。

      dotNetInstallerダウンロード
      git.hubサイト (dotNetInstaller 2.4
      dotNetInstaller公式サイト

      dotNetInstallerも本格的なインストーラ作成に対応しているが、ここでは単に言語切り替えを実現するだけ。dotNetInstallerの設定でmsiをsetup.exeに組み込みこむ。setup.exe実行で、これをTempフォルダに展開し、展開後msiexecでmsiを実行する。このとき、日本語の場合、そうでない場合の二つの構成を用意しておき、言語設定に応じて使い分ける。

      この方法については次のWebページで紹介されており、サンプルもダウンロード可能。

      dotNetInstaller には非常に多くの設定がありますが、今回関係したところいがいはよくわかりません。

      dotNetInstallerはconfigurationファイルを作成するInstaller Editorと、exeを作るコマンド InstallerLinker.exe からなる。

      Installer Editor / Configurationファイル作成

      準備
      手順を単純化するため、どこかに作業フォルダを作成し、MySetup.msi をコピーする。
      コマンドプロンプトを起動し、そのフォルダに移動しておく。InstallerLinker.exe 起動はここから行うことし、以下の定数#APPPATHはこの場所を指す。

      dotNetInstaller起動
      dotNetInstallerをインストール後、スタートメニューからInstaller Editor起動。

      File > New
      • Logging > log_enabledをTrueに変更(推奨)
        log_fileの場所に出力される。
        • #TEMPPATHは標準では次の場所
          C:\Users\UserName\AppData\Local\Temp
      • UI > ui_levelをsilentに変更(デバッグ中はbasicでもよい)
      • Locale > lcidtype を User(またはUserExe以外)に変更
      Edit > Add > Configurations > Setup Configuration
      • Language > lcid_filterを”1041”(日本語)に設定
      • Runtime > complete_commandに次のコマンドを入力
        msiexec /i #CABPATH\MySetup.msi TRANSFORMS=":ja-JP.mst"
        • #CABPATHはdotNetInstallerが作成するTempフォルダ。標準では次の場所。
          C:\Users\UserName\AppData\Local\Temp\{GUID}
        • "ja-JP"の部分は定義済み定数”#OSLOCALE”で置換可能だが、msiに組み込まれているmstと一致しないとエラーになる。
        • UIを表示させる場合は、少なくともメッセージのAPPLICATION_NAMEを変更する。
      • Main Dialog > dialog_show_installed をFalseに変更
      • Runtime > show_progress_dialogをFalseに変更(Trueでもよい)
      "install:"を右クリック > Add > Embed > Embed File
      • sourcefilepathに次のパスを入力
        #APPPATH\MySetup.msi
      • targetfilepathにMySetup.msiを入力。
        #CABPATHからの相対パスで、別の名前でもよく、またサブフォルダを加えることもできるが、
        msiexec のパラメータと一致させること。
      もうひとつSetup Configurationを追加
      • Setup Configuration以下の手順を繰り返し、もうひとつSetup Configurationを追加する。
        • Language > lcid_filterを"!1041"(日本語以外)に設定
        • Runtime > complete_commandに次のコマンドを入力(TRANSFORMSなし)
          msiexec /i #CABPATH\WavCutterSetup.msi 
      configuration.xml保存
      • File > Save または Save As で準備で作成したフォルダ内に保存
      InstallerLinker.exe実行 / setup.exe作成
      • 標準では次のフォルダにインストールされている。
        C:\Program Files (x86)\dotNetInstaller\bin
      • 次の要領でInstallerLinker.exeを実行する。
        >set linkerFolder="C:\Program Files (x86)\dotNetInstaller\bin"
        >%linkerFolder%\InstallerLinker.exe /o:setup.exe /t:%linkerFolder%\dotNetInstaller.exe /c:configuration.xml /v+
      • setup.exeが出力される。