📦

C# - 自己解凍書庫 - 定義ファイル追加

2025/02/10に公開

はじめに

C# - 自己解凍書庫 - 基本機能 の続編です。

テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。

  • Windows Forms - .NET Framework 4.8

素材

Windows Forms アイコンとして下記を利用させて頂きました。

定義ファイル追加

「自己解凍書庫ランチャー」は、さまざな用途で自己解凍書庫を作成時に、再利用できることが望ましいので、汎用的な単機能とすべきです。

まず、「自己解凍書庫ランチャー」に対する個別の情報は、定義ファイルで設定可能とします。
次に、特殊な処理(簡易的なインストーラ機能)が必要な場合は、該当処理専用の実行可能形式ファイルを作成して、ZIPアーカイブ内に格納、解凍後、自動起動する形態とします。

このような動作を前提とした、定義ファイルを自己解凍書庫に追加します。

定義ファイル

定義ファイル設定項目として、考えられる項目を記載します。

  • 初期表示ダイアログの「タイトル」「説明文」
  • 初期表示ダイアログは利用しない(解凍後、実行可能形式ファイルを実行を前提)
  • 解凍先フォルダの既定値
  • 解凍後、自動起動する実行可能形式ファイル(解凍フォルダからの相対パス指定)
  • 解凍後、解凍データを削除するか否か

本記事では、初期表示ダイアログの「タイトル」「説明文」を、XML で下記のように指定するケースをサンプルとします。

Hoge.xml
<?xml version="1.0" encoding="utf-8"?>
<Settings>
  <Title>〇〇サンプルプロジェクト</Title>
  <Message>〇〇サンプルプロジェクトを解凍します。
解凍フォルダを指定して、「解凍実行」ボタンを押してください。</Message>
</Settings>

自己解凍書庫の構造

「自己解凍書庫ランチャー」「動作定義ファイル」「ZIPアーカイブ」の3ファイルから自己解凍書庫を作成しますが、単純に3ファイルを結合すると「動作定義ファイル」の終端確認が困難です。

このため、「動作定義ファイル」のサイズを固定長ファイルに出力して、下記4ファイルを結合することにします。

  • 「自己解凍書庫ランチャー」
  • 「動作定義ファイル」のサイズ(本記事では、8バイト固定ファイルとします)
  • 「動作定義ファイル」
  • 「ZIPアーカイブ」

上記手順に基づいた Windows バッチファイルを作成して実行します。

> MakeJoinFile ZipMelt.exe hoge.xml hoge.zip hoge.exe
MakeJoinFile.bat
echo off
@rem 遅延関数のおまじない
setlocal enabledelayedexpansion

@rem 第二引数(設定ファイル)ファイルサイズを6桁0埋め値に整形
@rem echo (\r\n付与)で8バイトファイルを作成
for %%i in (%2) do set SIZE=%%~zi
set NUMBER=000000%SIZE%
set NUMBER=!NUMBER:~-6,6!
echo %NUMBER%>%2.txt

@rem ファイル結合
copy /b %1 + %2.txt + %2 + %3 %4

サンプルコード

スタートアップルーチン

スタートアップルーチンで下記情報を作成して、Form コンストラクタに情報をセットする形態とします。

  • 「定義ファイル」 XML をワークファイルとして作成
  • 「ZIPアーカイブ」をワークファイルとして作成
Program.cs
static void Main()
{
  string myself = Assembly.GetExecutingAssembly().Location;  // 自プログラム
  string xmlFile = "Piyo.xml";       // ユニークなワークファイルとしてください
  string zipFile = "Piyo.zip";       // ユニークなワークファイルとしてください 

  using(var fs = new FileStream(myself, FileMode.Open, FileAccess.Read))
  {
    // PEヘッダーからプログラムサイズ算出
    long size = GetSizeFromPeHeader(fs);
    if (size < 0)
    {
      // ERROR - TODO
    }

    // FileStream 位置をプログラム末尾に設定
    fs.Seek(size, SeekOrigin.Begin);

    // FileStream 現在位置から定義ファイルを抽出
    long xmlsize = MyApp2XmlFile(fs, xmlFile);
    if (xmlsize <= 0)
    {
      // ERROR - TODO
    }

    // FileStream 位置を定義ファイル末尾に設定
    fs.Seek(size + xmlsize, SeekOrigin.Begin);

    // FileStream 現在位置からファイル末尾までをZIPファイルに抽出
    MyApp2ZipFile(fs, zipFile);
  }

  // メインフォーム起動
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  Application.Run(new Form1(xmlFile, zipFile));

  // 後処理
  if (File.Exists(xmlFile))
  {
    File.Delete(xmlFile);
  }
  if (File.Exists(zipFile))
  {
    File.Delete(zipFile);
  }
}
// FileStream 現在位置から定義ファイルを抽出
private static int MyApp2XmlFile(FileStream fs, string xmlFile)
{
  int tagsize = 8;    // TODO
  var tagbufs = new byte[tagsize];
  int xmlsize = -1;

  // 定義ファイルのサイズ取得
  fs.Read(tagbufs, 0, tagsize);
  int.TryParse(Encoding.UTF8.GetString(tagbufs), out xmlsize);

  // 定義ファイル抽出
  if (xmlsize > 0)
  {
    var xmlbufs = new byte[xmlsize];
    using (var fsxml = new FileStream(xmlFile, FileMode.Create, FileAccess.Write))
    {
      fs.Read(xmlbufs, 0, xmlsize);
      fsxml.Write(xmlbufs, 0, xmlsize);
    }
    // 先頭で取得したサイズ情報 8バイトを追加
    xmlsize += tagsize;
  }
  return xmlsize;
}

メインフォーム

コンストラクタで、xmlFile、zipFile を内部変数に格納した後、xmlFile をロードして、ダイアログ表示内容を変更します。

Form1.cs
private string MyXmlFile = null;
private string MyZipFile = null;

public Form1(string xmlFile, string zipFile)
{
  InitializeComponent();

  // 内部変数保持
  this.MyXmlFile = xmlFile;
  this.MyZipFile = zipFile;

  // XML → オブジェクト
  var obj = LoadFromFile<XmlSettings>(MyXmlFile);

  // ダイアログ表示項目を XML 指定値で更新
  this.Text = obj?.Title ?? this.Text;
  lblMessage.Text = obj?.Message ?? lblMessage.Text;
}
// XML → オブジェクト
private T LoadFromFile<T>(string filePath) where T : class
{
  T obj = null;
  XmlSerializer serializer = new XmlSerializer(typeof(T));

  // ファイル確認
  if (File.Exists(filePath))
  {
    // デシリアライズ
    using (var fs = new FileStream(filePath, FileMode.Open))
    {
      obj = serializer.Deserialize(fs) as T;
    }
  }
  return obj;
}
[Serializable]
[XMLRoot Name="Settings"]
public class XmlSettings
{
  [XmlElement]
  public string Title { get; set; }
  [XmlElement]
  public string Message { get; set; }
}

次に「解凍実行」Click イベントハンドラで、ZIP解凍を行います。

Form1.cs
private void btnMelt_Click(object sender, EventArgs e)
{
  string targetFolder = txtTargetFolder.Text.Trim();         // ダイアログ指定値

  // ZIP解凍
  using (var arc = ZipFile.OpenRead(this.MyZipFile))
  {
    arc.ExtractToDirectory(targetFolder);
  }
}

出典

本記事は、2025/02/10 Qiita 投稿記事の転載です。

C# - 自己解凍書庫 - 定義ファイル追加

Discussion