XORveR.com の日記

XORveR.com の公式ブログです。

システム状態を参照するICommandって有りや無しや

まえがき

システムの状態によって振る舞いを変える画面要素があったとします。
ぶっちゃけるなら、クリップボードにイメージが入っている場合にのみ有効になるメニューアイテムなんですが。
こいつを実装するのにちょっと悩んでいたりします。

理由

なぜかと言えば、ViewModelは単体テスト対象。という主義。
その単体テストでクリップボードを使うというのは避けたいからです。
テスト実行中に手動でクリップボードを使ってしまうと誤作動しますから。

方法案1 - ガリゴリとViewにコードを書く。

理由にあるのと同様の理由でViewに、クリップボードからイメージを取得してViewModelに渡す部分は書く必要があります。
よって、毒を食うなら皿までの精神でViewに全部書いちゃえ。

方法案2 - ViewModelにICommandで実装。

その部分はテストしなけりゃいーじゃん。
どーせViewはOpenCoverのカバー範囲外なんだから、パーセンテージには影響しないよ!

方法案2改 - できるか分かんないけどViewModelに添付プロパティとしてICommand付けちゃえ。

これならxamlでCommandをバインドできるよ、やったね!
・・・なんでそんなシチ面倒くさいことせにゃならんの。馬鹿なの死ぬの?
いや、xamlの記法が統一できるというメリットはデカイかも。うーん

結論

やっぱ簡潔さから見て方法1かなーと。

多重起動しないアプリを作ってみた。

まえがき

Common.Util.IPC の使用法のサンプルです。

メリット

単一のインスタンスで実行するアプリケーションを容易に開発できます。

サンプルプロジェクト

Unique プロジェクト

動作について

これを実行すると、多重起動を検知して後続での実行時引数を先行する実行に送信して終了します。
サンプルは実行時引数を接続して MessageBox に出します。

ソースコード

App.xaml.cs

using System;
using System.Windows;
using Common.Util;
using Common.Util.IPC;

namespace Unique {
	/// <summary>
	/// App.xaml の相互作用ロジック
	/// </summary>
	public partial class App : Application, IDisposable {
		private UniqueApplicationInstance unique;
		private MainWindow window;
		private LifeCycleManager lifecycles = new LifeCycleManager();

		public App() {
			Startup += Application_Startup;
			Exit += Application_Exit;
		}

		private void Application_Startup(object sender, StartupEventArgs e) {
			lifecycles.Register(
				(o) => {
					unique = new UniqueApplicationInstance();
				},
				(o) => {
					unique.Dispose();
					unique = null;
				}
			);
			if (unique.IsUniqueApplication == false) {
				// コマンドライン引数を送信
				unique.RaiseStartupNextInstance(e.Args);
				// 終了
				Shutdown();
			} else {
				lifecycles.Register(
					(o) => { Logger.GetLogger().Info("Application Startup"); },
					(o) => { Logger.GetLogger().Info("Application Shutdown"); }
				);

				// メインウィンドウ作成
				lifecycles.Register(
					(o) => {
						window = new MainWindow();
					},
					(o) => {
						window = null;
					}
				);

				// ウィンドウ表示
				window.Show();
				// コマンドライン引数の受信イベントを登録
				unique.OnNextInstanceStartupDetected += NextInstanceStartup_Detected;
			}
		}
		private void Application_Exit(object sender, ExitEventArgs e) {
			Dispose();
		}

		/// <summary>
		/// 多重起動したインスタンスからコマンドライン引数が送られてきた
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void NextInstanceStartup_Detected(object sender, StartupNextInstanceEventArgs e) {
			string str = "";
			foreach (object o in e.Parameters) {
				str += o.ToString() + " ";
			}
			MessageBox.Show(str.TrimEnd());
		}

		#region IDisposable Support
		private bool disposedValue = false;
		protected virtual void Dispose(bool disposing) {
			if (!disposedValue) {
				disposedValue = true;
				if (disposing) {
					lifecycles.Dispose();
				}
			}
		}
		public void Dispose() {
			Dispose(true);
		}
		#endregion
	}
}

VisualStudioでjsonの文字列リソースを作ってみた。その2

まえがき

前回 は、json文字列リソースの作成までを書きました。
その使い方について説明します。

メリット

未対応な言語での使用において、json ファイルを手作業で編集することで容易に翻訳作業を行えます。

サンプルプロジェクト

Texts プロジェクト

動作について

これを実行すると、実行ファイル(Texts.exe)のプロパティに記載された企業名・プロダクト名でフォルダを作成します。
フォルダ:C:\Users\ユーザ名\AppData\Local\企業名(XORveR)\プロダクト名(Texts)

このフォルダに、実行環境のカルチャ(ja-JP)を元に文字列リソースファイル(Strings.ja-JP.json)を作成します。
これから実行ファイルで表示される各部の文字列を取得しています。

対応外のカルチャの場合

もし Texts/Resources に存在しないリソースだった場合には Strings.en-US.json をコピーします。
ですから、コピーされたカルチャ用ファイルとして json の中身を編集することで、自国語への対応が可能となります。
残念ながらテキストの表示方向には対応していませんので、一部の言語には不都合が発生します。

その他

本サンプルでは利用しませんでしたが、Commonプロジェクトの
TxtExtension を利用して、文字列リソースをバインドすることも可能です。

コードサンプル

まえがき

コードサンプルとして Microsoft の Visual Studio Team Services に Common ライブラリを置いてみました。

Commonソリューション

ライセンス

MIT ライセンスです。
コピーライト要件は消してしまうかもしれません。(他からパクったコードも多いですので)
[license.txt]

本サンプルは MIT ライセンスのもとで配布されるものとします。

下記の条件を受け入れていただけるのであれば、誰でも自由に無料で、このソフトウェアをつかっていただくことができます。

このソフトウェアをコピーしてつかったり、配布したり、変更を加えたり、変更を加えたものを配布したり、商用利用したり、有料で販売したり、なんにでも自由につかってください。

このソフトウェアの著作権表示(「Copyright (c) 2017 XORveR」)と、このライセンスの全文を、ソースコードのなかや、ソースコードに同梱したライセンス表示用の別ファイルなどに掲載してください。

このソフトウェアにはなんの保証もついていません。たとえ、このソフトウェアを利用したことでなにか問題が起こったとしても、作者はなんの責任も負いません。 

画面遷移サンプル

画面遷移のパターンとして、ユニークなウィンドウの中のコンテンツを画面遷移するサンプルが同梱してあります。

Transitionプロジェクト

コンセプト

遷移する画面は、UserControl で記述してあります。
そして、遷移元の画面から MainWindow に対して遷移先の画面のインスタンスを渡すことで画面遷移を実現します。
その理由は、『画面遷移は View の役割だ』というコンセプトによります。
画面遷移を View に専任させることで、ViewModel の単体テストは何の問題も無くなります。

UserControl1.xaml.cs

using System.Windows.Controls;
using Transition.ViewModel;

namespace Transition.View {
	/// <summary>
	/// UserControl1.xaml の相互作用ロジック
	/// </summary>
	public partial class UserControl1 : UserControl {
		ViewModel1 vm = new ViewModel1();
		public UserControl1() {
			InitializeComponent();
			DataContext = vm;
		}
		/// <summary>
		/// 操作イベント
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void valueButton_Click(object sender, System.Windows.RoutedEventArgs e) {
			vm.AddOne();
		}
		/// <summary>
		/// 画面遷移イベント
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void nextButton_Click(object sender, System.Windows.RoutedEventArgs e) {
			// 遷移先に何かパラメータを送りたい場合にはここで値をセット
			MainWindow.Instance.ViewChangeTo(new UserControl2());
		}
	}
}

VisualStudioでjsonの文字列リソースを作ってみた。その1

まえがき

dotnetでの文字リソースは扱いが厄介なので、利用者が勝手に編集できるようにjsonファイルで提供を考えた。

問題点

しかし、リソースのキーは

public const string Product_Name = "Product_Name";

といった風にしておきたい。
理由は言うまでもなく文字列リソースとコードの連携のためです。

解決策

T4テキストテンプレートで、CSharpコードからjsonファイルを作ればいーじゃん。

実装

CSharpコード

MessageAttribute を設けて、文字リソースの内容を記入しておきます。
[Strings.cs]

using System;

namespace Common.I18n {
	[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
	public class MessageAttribute : Attribute {
		private string message;
		private bool isseparate;
		public MessageAttribute(string message, bool isseparate = false) {
			this.message = message;
			this.isseparate = isseparate;
		}
		public string Message { get { return this.message; } }
		public bool IsSeparate { get { return this.isseparate; } }
	}
	/// <summary>
	/// Strings.tt で json に変換するクラス
	/// </summary>
	public static class Strings {

		/// <summary>
		/// product name
		/// </summary>
		[Message("File Protector XORveR")]
		public const string Product_Name = "Product_Name";

		[Message("English", true)]
		public const string ResText = "ResText";

		[Message("Entry Files", true)]
		public const string ProcessTab_Header = "ProcessTab_Header";
		[Message("Completed Files")]
		public const string CompletedTag_Header = "CompletedTag_Header";
		[Message("Failed Files")]
		public const string FailedTab_Header = "FailedTab_Header";

・・・・

		// company name
		[Message("XORveR.com", true)]
		public const string Company_Name = "Company_Name";
	}
}

T4テキストテンプレート

ファイルのプロパティでのカスタムツールの設定には TextTemplatingFileGenerator を指定しておきます。
[String.en-US.tt]

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.CodeDom.Compiler" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".json" encoding="utf-8" #>
<# 
	var filename = this.Host.ResolvePath("Strings.cs");
	var list = new List<string>();

	CodeDomProvider cscp = CodeDomProvider.CreateProvider("CSharp");
	var source = File.ReadAllText(filename);
	var param = new CompilerParameters();
	param.GenerateInMemory = true;
	var cr = cscp.CompileAssemblyFromSource(param, source);
	Assembly asm = cr.CompiledAssembly;

	Type att = null;
	PropertyInfo Message = null;
	PropertyInfo IsSeparate = null;
	foreach (Type t in asm.DefinedTypes) {
		if (t.Name == "MessageAttribute") {
			att = t;
			Message = t.GetProperty("Message");
			IsSeparate = t.GetProperty("IsSeparate");
			break;
		}
	}

	Type type = null;
	foreach (Type t in asm.DefinedTypes) {
		if (t.Name == "Strings") {
			type = t;
			break;
		}
	}

	foreach (FieldInfo field in type.GetFields()) {
		// 本当は、プロパティとかの順番は無保証・・・
		var mesatt = field.GetCustomAttribute(att);
		var message = (string)Message.GetValue(mesatt);
		var isseparate = (bool)IsSeparate.GetValue(mesatt);
		if (isseparate) {
			list.Add("");
		}
		if (field.Name.Equals("Company_Name")) {
			list.Add("  \"" + field.Name + "\": \"" + message.Replace("\"", "\\\"") + "\"");
		} else {
			list.Add("  \"" + field.Name + "\": \"" + message.Replace("\"", "\\\"") + "\",");
		}
	}
#>
{
<#
	foreach (string line in list) {
#>
<#= line #>
<#
	}
#>
}

jsonファイル

[Strings.en-US.json]

{
  "Product_Name": "File Protector XORveR",

  "ResText": "English",

  "ProcessTab_Header": "Entry Files",
  "CompletedTag_Header": "Completed Files",
  "FailedTab_Header": "Failed Files",

  "Password_Newface": "new password",

・・・・

  "Company_Name": "XORveR.com"
}

あとがき

jsonは作ったけど、それからどーする?という話は、また別の機会に。
では!

ニッチ過ぎて誰からも「いいね」されなかったシリーズ2 (wsf編その1)

まえがき

シリーズ第一弾は意外と好評だったので、第二弾を早々に書きます。

wsf!!!

さて、wsfって使われていないように見られますが・・・意外なことに物凄くデキる子なんですよ。
Windows でテキスト処理その他と言われたら、何を提案しますか?
java? c#? いえいえ perl も ruby も差し置いて、私は wscript (WSH) を推します。
というか、アソコの企業でもアチラの企業でも、私の書いた wsf は今も使われています。(はず)
五年も前に書いたコードのことを聞いたら、まだ現役と言われましたし。

wsf編その1では、お馴染みの DosPromptHere を wsf で書いたものを披露します。
DosPromptHere ならば実用品で何を行うモノなのかが明確ですので、サンプルとして手頃でしょう。

wsf編『その1』?

今回のサンプルは『基本形』です。

まず、今回はサブルーチンのライブラリを使っていません。
ライブラリ化は単体テストを書くためにも応用形としては外せませんね。
ただ、そこらはサラっと流す予定です。

多態性の利用もスクリプトなんですから、それもパス。

それより何より、wsfの恐るべき機能(というかXMLであること)を利用していないからです。
それについては後日までの隠し玉です。フフフ

技術としての注目点

本サンプルでは、以下の点が目新しい技術だろうと思っています。

  1. WHS の job による処理の分離(インストール/アンインストール部と実行処理部)
  2. レジストリを使うため、勝手に再実行して UAC を受ける方法(runas)

総合的にアプリとして使える枠組みこそがサンプルとしての意義です。
VBS から ActiveX を叩くなんてのは、全然目新しくないですよね?

処理の分離と、レジストリ操作の手順としての UAC 通過手順は、wsf の基本です。
ただでさえ UAC は一手間ですから、管理者として実行なんてのは自動で出来ないと面倒です。

続きを読む

ニッチ過ぎて誰からも「いいね」されなかったシリーズ

まえがき

元文書は5年前の日付(さすがにMUIはある2.0台)なので、現在のNSISにも有効なのか不明です。
NSIS スクリプトをそろそろ書かないとならないため、おさらいしておく目的で整理し転記します。

ただ、WIX とかいうものがあるそうで、・・・まあ記事のネタに。

NSISスクリプトでの関数の作り方

NSIS の構文は基本的にアセンブラ言語がベースです。
そのため、変数操作にはスタックを多用して実装するスタイルになります。
CASL と比べるとスタック要素の交換命令である Exch の存在で、ずいぶん楽になっています。

● スタック操作

スタック操作命令は以下のものが使用可能です。

	Push (変数|レジスタ|リテラル)
		オペランドを「スタックトップ」にプッシュします。
	Pop (変数|レジスタ)
		「スタックトップ」をオペランドにポップします。
	Exch (変数|レジスタ|リテラル|正整数値)
		「スタックトップ」とオペランドを交換します。

		Exchは特殊で、正整数値の場合は異なる動作をします。
		正整数値はオフセットです。
		スタックトップとオフセット位置のアイテムを交換します。

このように命令が曲者です。
基本的には「スタックトップ」以外は操作できないからです。
スタックに積まれた値を利用したければ、Exch 命令でスタックトップに引き出して使う必要があります。

さあ、めくるめくアセンブラ風プログラミングの世界にダイブ!

● 関数の書き方

再利用のため、関数の機構が用意されています。
ただし、関数呼び出しに引数を渡す構文はありません。
フレームポインタのような高級な機能すらありません。

ですので、手書きのスタック操作でフレームを構築します。(凶悪)

実例として

	func(p0,p1,p2,p3) {
		var ret;
		var v1;
		var v2;

		(処理)

		return ret;
	}

というような関数を実現したい場合、以下のように設計していきます。

○ 1.入出力を決めます。

関数の呼び出し前後でどのような変数状態であるかということです。

  • 呼び出し前に、引数をスタックに Push しておく。
  • 呼び出し後に、結果はスタックに Push されている。
  • レジスタは呼び出し前後で変わらない。

というのが定石です。

ここで制約事項をまとめます。

	・ユーザ変数と衝突する可能性があるために名前付き変数は使用しません。
	 ですから使えるのはスタックとレジスタ ($0~$9,$R0~$R9) のみとなります。

	・引数はレジスタに取り出さなければ使用できません。
	 よって現在のレジスタの内容は逆にスタックに退避する必要があります。

	・自動変数として使用するレジスタは破壊されてしまいます。
	 よって現在のレジスタの内容はスタックに退避する必要があります。
	 これはレジスタ退避データと呼びましょうか。

	・レジスタ退避データは処理の最後でレジスタに復元します。
	 スタックポイントは、Pop することで調整します。

	・話が前後してしまいますが、関数内部で結果の格納用として使用するレジスタは
	 処理の最後でレジスタ退避データと Exch で交換する必要があります。
	 つまりスタックの底、スタックボトムの直前に位置する必要があります。
	 ※ ただし、定数値を返す場合は定数を Push するだけなので不要です。

実例の場合、引数が四個、変数が二個、戻り値が一個の場合なので、

	呼び出し前:スタックにp0~p3が積まれている。
	呼び出し中:$R0(ret), $R1(p0), ... $R4(p3), $R5(v1), $R6(v2) を使用する。
	呼び出し後:スタックからレジスタ復元し、$R0(ret)を交換して終了。

とします。


具体的に呼び出し方は

	Push p0
	Push p1
	Push p2
	Push p3
	Call func
	Pop $結果

というコードになります。

関数内でのスタックの状態は呼び出し前の時点で

		[+0]	: p3
		[+1]	: p2
		[+2]	: p1
		[+3]	: p0
		[+4]	: スタックボトム、もしくは別の処理で使用中

呼び出し中は

		[+0]	: $R6 退避データ
		[+1]	: $R5 退避データ
		[+2]	: $R4 退避データ
		[+3]	: $R3 退避データ
		[+4]	: $R2 退避データ
		[+5]	: $R1 退避データ
		[+6]	: $R0 退避データ
		[+7]	: スタックボトム、もしくは別の処理で使用中

呼び出し後には

		[+0]	: res
		[+1]	: スタックボトム、もしくは別の処理で使用中

となります。

これを元に、実装してみます。

○ 2.処理のためのレジスタを準備します。

結果の格納用として使うレジスタはスタックのボトム直前に退避しておきます。
これは最後に Exch で交換するためです。

	Push $R0	; $R0 をスタックに退避 ($R0 を res として確保)

		[+0]	: $R0 退避データ
		[+1]	: p3
		[+2]	: p2
		[+3]	: p1
		[+4]	: p0
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch 4		; [+0] と [+4] を交換

		[+0]	: p0
		[+1]	: p3
		[+2]	: p2
		[+3]	: p1
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

引数データをレジスタと交換していきます。

	Exch $R1	; [+0] と $R1 を交換 ($R1 = p0)

		[+0]	: $R1 退避データ
		[+1]	: p3
		[+2]	: p2
		[+3]	: p1
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

レジスタとの交換はスタックの先頭としかできませんので奥のアイテムと交換します。

	Exch 3		; [+0] と [+3] を交換

		[+0]	: p1
		[+1]	: p3
		[+2]	: p2
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

引数の全てを同様に処理します。

	Exch $R2	; [+0] と $R2 を交換 ($R2 = p1)

		[+0]	: $R2 退避データ
		[+1]	: p3
		[+2]	: p2
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch 2		; [+0] と [+2] を交換

		[+0]	: p2
		[+1]	: p3
		[+2]	: $R2 退避データ
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch $R3	; [+0] と $R3 を交換 ($R3 = p2)

		[+0]	: $R3 退避データ
		[+1]	: p3
		[+2]	: $R2 退避データ
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch 1		; [+0] と [+1] を交換

		[+0]	: p3
		[+1]	: $R3 退避データ
		[+2]	: $R2 退避データ
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch $R4	; [+0] と $R4 を交換 ($R4 = p3)

		[+0]	: $R4 退避データ
		[+1]	: $R3 退避データ
		[+2]	: $R2 退避データ
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

自動変数は、使用するレジスタを退避するだけです。

	Push $R5	; スタックに $R5 を退避 ($R5 を v1 として確保)
	Push $R6	; スタックに $R6 を退避 ($R6 を v2 として確保)

		[+0]	: $R6 退避データ
		[+1]	: $R5 退避データ
		[+2]	: $R4 退避データ
		[+3]	: $R3 退避データ
		[+4]	: $R2 退避データ
		[+5]	: $R1 退避データ
		[+6]	: $R0 退避データ
		[+7]	: スタックボトム、もしくは別の処理で使用中

これで、$R0(ret), $R1(p0), ... $R4(p3), $R5(v1), $R6(v2) を使用することができるようになりました。

○ 3.関数の処理の記述

以下のレジスタを変数として使用して、処理を記述します。

	$R0		: res
	$R1		: p0
	$R2		: p1
	$R3		: p2
	$R4		: p3
	$R5		: v1
	$R6		: v2
○ 4.return の書き方

自動変数として使っていたレジスタを復元します。

結果以外は Pop して呼び出し前のレジスタを復元します。
結果はスタックに積まれる形なので交換します。

	Pop $R6		; スタックから $R6 を復元
	Pop $R5		; スタックから $R5 を復元
	Pop $R4		; スタックから $R4 を復元
	Pop $R3		; スタックから $R3 を復元
	Pop $R2		; スタックから $R2 を復元
	Pop $R1		; スタックから $R1 を復元
	Exch $R0	; スタック[0] ($R0) と res を交換

これでスタックは

		[0]	: res
		[+1]	: スタックボトム、もしくは別の処理で使用中

となり、インタフェース要件を満たします。

● まとめ

最後に関数の構文として纏めます。

	Function func
		Push $R0	; $R0 をスタックに退避 ($R0 を res として確保)
		Exch 4		; [+0] と [+4] を交換
		Exch $R1	; [+0] と $R1 を交換 ($R1 = p0)
		Exch 3		; [+0] と [+3] を交換
		Exch $R2	; [+0] と $R2 を交換 ($R2 = p1)
		Exch 2		; [+0] と [+2] を交換
		Exch $R3	; [+0] と $R3 を交換 ($R3 = p2)
		Exch 1		; [+0] と [+1] を交換
		Exch $R4	; [+0] と $R4 を交換 ($R4 = p3)
		Push $R5	; スタックに $R5 を退避 ($R5 を v1 として確保)
		Push $R6	; スタックに $R6 を退避 ($R6 を v2 として確保)

		(処理内容)

		Pop $R6		; スタックから $R6 を復元
		Pop $R5		; スタックから $R5 を復元
		Pop $R4		; スタックから $R4 を復元
		Pop $R3		; スタックから $R3 を復元
		Pop $R2		; スタックから $R2 を復元
		Pop $R1		; スタックから $R1 を復元
		Exch $R0	; スタック[0] ($R0) と res を交換
	FunctionEnd

実際は条件によって色々な解が存在しますので、この実装は例に過ぎません。
応用として、複数の結果を返す(スタックに複数の値を積んで戻る)実装もあります。

● マクロの使用

実は、インストールセクションで使える関数とアンインストールセクションで使える関数は明確に命名規則が定められています。
アンインストールセクションから呼ばれる関数は接頭語として un. が付いている必要があります。(本稿の例ではun.funcとなります)

両方で使用するための書き方として、マクロを使用するのが定石です。
マクロはプリプロセッサによりインラインで展開されます。

ですから、通常では関数は

	; マクロ定義
	!macro func un
	; 関数定義
	Function ${un}func
		・・・
	FunctionEnd
	!macroend

	; インストール時に使用される $func としての展開
	!insertmacro func ""

	; アンインストール時に使用される $un.func としての展開
	!insertmacro func "un."

という書き方をすることが多いです。

これで同じ内容で両セクションで使用できる関数に展開され、アンインストール
セクションからも un.func として呼び出せるようになります。

では!


予告

シリーズ第二弾は wsf の書き方になる予定!