CSIDLではなく KNOWNFOLDERIDをつかう。GetSpecialFolderPathではなくGetKnownFolderPathをつかう

久しぶりにWindows規定のフォルダーを調べる必要があって、CSIDLの何だっけと調べてみた。

MicrosoftCSIDLに関するページをみると、最初に以下のように書かれている。

注意
Vista Windows、これらの値は KNOWNFOLDERID 値に置き換えられています。 新しい定数とそれに対応する CSIDL 値の一覧については、このトピックを参照してください。 便宜上、CSIDL 値ごとに対応する KNOWNFOLDERID 値もここに示されています。

CSIDL システムは、互換性上の理由Windows Vista でサポートされています。 ただし、新しい開発では 、CSIDL 値ではなく KNOWNFOLDERID 値を使用する必要があります。

少し日本語が怪しいがKNOWNFOLDERIDを使うようにしましょうとのことだ。

フォルダーを調べる関数SHGetSpecialFolderPathのページではトップに以下のように書かれている。

[SHGetSpecialFolderPath is not supported. Instead, use SHGetFolderPath.] 

SHGetSpecialFolderPathはサポートされていないのでSHGetFolderPathを使用してくださいとのこと。

じゃあ、SHGetFolderPathのページはというと、同じくトップに以下のように書かれている。

Note  As of Windows Vista, this function is merely a wrapper for SHGetKnownFolderPath. The CSIDL value is translated to its associated KNOWNFOLDERID and then SHGetKnownFolderPath is called. New applications should use the known folder system rather than the older CSIDL system, which is supported only for backward compatibility.

要は、SHGetFolderPathはSHGetKnownFolderPathのラッパーで、CSIDL値はKNOWNFOLDERIDに内部で変換して渡しているよとのこと。

Windows Vista以降であるならば、CSIDL値を使わずKNOWNFOLDERIDを使い、パスを得るにはSHGetSpecialFolderPathを使わずSHGetKnownFolderPathを使えと言うことらしい。
Windows11はテストをしていないので分からないが、現時点で手元のWindows10ではCSIDL+GetSpecialFolderPathの組み合わせでも期待通りの結果を得ることができる。

ざっくりとコードで比較してみる。なお下記のコードをコンパイルするにはどちらもshlobj.hをインクルードする必要がある。
まずCSIDL+SHGetSpecialFolderPathでのパターン

  wchar_t wPath[MAX_PATH];
  HRESULT hr = ::SHGetSpecialFolderPath( NULL, wPath, CSIDL_DESKTOP, FALSE );
  if( SUCCEEDED( hr ) )
  {
    // wPathを使って作業
  }

次にKNOWNFOLDERID+SHGetKnownFolderPathでのパターン

  wchar_t* pwPath;
  HRESULT hr = ::SHGetKnownFolderPath( FOLDERID_Desktop, 0, NULL, &pwPath );
  if( SUCCEEDED( hr ) )
  {
    // wPathを使って作業
  }
  ::CoTaskMemFree( pwPath );

SHGetKnownFolderPathの最初の引数は、REFKNOWNFOLDERID型となっておりKNOWNFOLDERIDのポインターを渡すようになっているが、定義済みのKNOWNFOLDERID定数はそのまま書ける(つまりポインターになっている)。

CSIDL+SHGetSpecialFolderPathでは、パスの配列はこちらで用意する必要がある。まずないとは思うがMAX_PATH以上のパス文字列の場合は失敗するか、メモリー領域の破壊が発生する。
一方、KNOWNFOLDERID+SHGetKnownFolderPathでは、パスの配列はSHGetKnownFolderPath側で用意されポインターを返す。
このため、使用後には必ず廃棄しなければならない(CoTaskMemFree関数)。

KNOWNFOLDERID+SHGetKnownFolderPathはVista以降でなければ動作しないが、これからのプログラムでXP以前をサポートすることもないだろうし、素直にKNOWNFOLDERID+SHGetKnownFolderPathでコーディングした方が良さそうだ。

2022-05-28 追記

リンクエラーで未解決の外部シンボルと言われる場合は、以下を追加すること。

#pragma comment(lib, "Shell32")

enumを基に変換関数を内蔵したオブジェクトをつくる

enumで定義されたデータをシリアライズするときは、何らかの数値(あるいは文字列)と相互変換する。
相手がintでC言語であれば、単純にキャストで相互変換もできる。C++であればstatic_castを使うこともあるだろう。

  enum en { e1, e2, e3 };
  
  en e = e3;
  int n = (int)e; // C言語、C++言語
  int m = static_cast<int>(e); // C++言語

  int s = 2;
  en f = (en)s; // e3
  en g = static_cast<en>(s); // e3

但し、enumシリアライズでつきものなのは、その仕様が変わった場合、互換性が損なわれることや、範囲外の値のチェックについて、メンテナンス性に欠けるといった点だ。

  enum en { e1, e4, e2, e3 }; // 仕様が変わった
  
  en e = e2;
  int n = (int)e; // C言語、C++言語
  int m = static_cast<int>(e); // C++言語

  int s = 2;
  en f = (en)s; // e2!
  en g = static_cast<en>(s); // e2!

実用的なコードを記述するときに、シリアライズでチェックするコードとenumの宣言が別のファイルで記述されることはよくあることで、enumの定義を変えたときに、対応するコードを更新し忘れすることは良くあることだ。
そのようなことにならないよう、enumの宣言で定数定義する方法もある。

  enum en { e1=0, e4=3, e2=1, e3=2 }; // 定数定義

しかし、古いプログラムでe4がどう扱われるか? と言った問題がある。enumで定義が大量にある場合などは悪夢だろう。

そういう点で、enumと例えばintの相互変換をenumの宣言の近くに配置できれば、メンテナンス性が向上することが期待できる。

前回の方法で、enumとintの相互変換ができる元のenumと互換なオブジェクトを定義してみる。
もちろん、変換関数を双方向分、enumの定義の近くで定義しても良いが、その方法は簡単なので割愛する。

template <typename T>
class TSetFunc
{
  public:
    static int to_int( T   a ) { return static_cast<int>(a); }
    static T   to_T(   int n ) { return static_cast<T>(n); }
};

template <typename T, class C>
class TBasicSetParam
{
  private:
    T t;
  public:
    TBasicSetParam() : t() {}
    TBasicSetParam(            const T                lhs ) { t = lhs; }
    TBasicSetParam(            const int              lhs ) { t = C::to_T( lhs ); }
    TBasicSetParam(            const TBasicSetParam&  lhs ) = default;
    TBasicSetParam(                  TBasicSetParam&& rhs ) = default;
    TBasicSetParam& operator=( const TBasicSetParam&  lhs ) = default;
    TBasicSetParam& operator=(       TBasicSetParam&& rhs ) = default;
    ~TBasicSetParam()                                       = default;

    operator T() { return t; }
    int to_int() { return C::to_int( t ); }
};
template <typename T>
using TSetParam = TBasicSetParam<T, TSetFunc<T>>;

enumを定義して、その変換オブジェクトを実装する。

enum class ec { e1, e2, e3 };

template<>
class TSetFunc<ec>
{
  public:
    static int to_int( ec a ) {
      switch( a )
      {
        case ec::e1: return  1;
        case ec::e2: return  2;
        case ec::e3: return  3;
        default:     return -1; // throw false;とすれば例外を投げることも
      }
    }
    static ec to_T( int n ) {
      switch( n )
      {
        case  1: return ec::e1;
        case  2: return ec::e2;
        case  3: return ec::e3;
        default: return ec::e1;
      }
    }
};
using Ec = TSetParam<ec>;

利用コードは、以下のような感じになる。

  Ec E;

  E = ec::e1;
  E = 2;
  if( E == ec::e2 )
    E = ec::e1;
  if( ec::e1 == E )
    E = ec::e3;

  int n = E.to_int();
  ec e = E;

enumの定義外もdefaultでどうするか決めることができる。
enumからintへのswitch文でわざとdefaultを定義しなければ、全てのcaseが記述されなければコンパイル時に警告を発生させることもできる。
また、オブジェクトEcは元のecのオブジェクトとほぼ同等に扱える。
switchの遅さが気になるのであれば、別に早いコードに書き換えることも可能だ。

intの代入は副作用が怖いので、TBasicSetParam( const int lhs )を定義しないで、staticな変換関数を準備する方が良いかもしれない。

template <typename T, class C>
class TBasicSetParam
{
  private:
    T t;
  public:
    TBasicSetParam() : t() {}
    TBasicSetParam(            const T                lhs ) { t = lhs; }
    //TBasicSetParam(            const int              lhs ) { t = C::to_T( lhs ); }
    static TBasicSetParam int_to( const int lhs ) { return TBasicSetParam( C::to_T(lhs) ); }
    TBasicSetParam(            const TBasicSetParam&  lhs ) = default;
    TBasicSetParam(                  TBasicSetParam&& rhs ) = default;
    TBasicSetParam& operator=( const TBasicSetParam&  lhs ) = default;
    TBasicSetParam& operator=(       TBasicSetParam&& rhs ) = default;
    ~TBasicSetParam()                                       = default;

    operator T() { return t; }
    int to_int() { return C::to_int( t ); }
};
template <typename T>
using TSetParam = TBasicSetParam<T, TParamFunc<T>>;
  Ec E;

  E = Ec::int_to(2);

オブジェクト毎に内部インターフェースをカスタマイズする方法

複数のオブジェクトクラスに対して、同じ動作をさせるクラスの設計をすることがある。
例えば、ファイルへのIO処理はいろいろなオブジェクトを文字列かバイナリに相互変換させるクラスあるいは関数を設計する。
毎度個別に記述するのも良いが、もう少し簡便になるようにしたいことも多い。
どちらを選ぶかは、ケースバイケースだと思う。

さて、そのような汎用クラスの設計でC++言語ならテンプレートを用いることができる。ラムダ式? 自分にはよく分からないのでパス。
以下のテンプレート宣言は、「bool g(T a)」の部分をオブジェクト(T)毎に最適化することを想定した。

template <typename T>
class A1
{
  private:
    bool g( T a ) { return true; }
  public:
    A1() {}
    A1(            const A1&  lhs ) = default;
    A1(                  A1&& rhs ) = default;
    A1& operator=( const A1&  lhs ) = default;
    A1& operator=(       A1&& rhs ) = default;
    virtual ~A1()                   = default;

    bool f( T a ) { return g(a); }
};

例えばオブジェクトをintにするときには、以下のように追加で記述する。

template<>
class A1<int>
{
  private:
    bool g( int n ) { return ( 0 == n ); }
  public:
    A1<int>() {}
    A1<int>(            const A1<int>&  lhs ) = default;
    A1<int>(                  A1<int>&& rhs ) = default;
    A1<int>& operator=( const A1<int>&  lhs ) = default;
    A1<int>& operator=(       A1<int>&& rhs ) = default;
    virtual ~A1<int>()                        = default;

    bool f( int a ) { return g(a); }
};

利用側では、こんな感じ

  A1<int> a1;
  bool b0 = a1.f( 0 ); // true
  bool b1 = a1.f( 1 ); // false

この実装では特化テンプレートを定義することで行っているが、この場合オブジェクト毎に全て書き直しなのであまりうれしくない。
オブジェクトに関係しない部分をベースクラスにしてpublic継承もできるが、オブジェクト毎にテンプレート化する部分は毎度書くのでメンテナンス性も良くない。

では、派生クラスで実装してみるのはどうだろうか。
オブジェクト毎に特有の部分を仮想関数で実装する。

template <typename T>
class A2
{
  private:
    virtual bool g( T a ) = 0;
  public:
    A2() {}
    A2(            const A2&  lhs ) = default;
    A2(                  A2&& rhs ) = default;
    A2& operator=( const A2&  lhs ) = default;
    A2& operator=(       A2&& rhs ) = default;
    virtual ~A2()                   = default;

    bool f( T a ) { return g(a); }
};

オブジェクトをintにするときには、以下のように追加で記述する。

class B2 : public A2<int>
{
  private:
    virtual bool g( int n ) override { return ( 0 == n ); }
  public:
    B2() : A2<int>() {}
    B2(            const B2&  lhs ) = default;
    B2(                  B2&& rhs ) = default;
    B2& operator=( const B2&  lhs ) = default;
    B2& operator=(       B2&& rhs ) = default;
    virtual ~B2()                   = default;
};

利用側では、こんな感じ

  B2 b2;
  bool b0 = b2.f( 0 ); // true
  bool b1 = b2.f( 1 ); // false

記述は派生クラスの実装で最低限必要な分だけになった。呼び出しは仮想関数の分だけ少しオーバーヘッドがある。

特化する部分を追い出したテンプレートにする方法もある。

template <typename T, class C>
class A3
{
  private:
    bool g( T a ) { return C::f( a ); }
  public:
    A3() {}
    A3(            const A3&  lhs ) = default;
    A3(                  A3&& rhs ) = default;
    A3& operator=( const A3&  lhs ) = default;
    A3& operator=(       A3&& rhs ) = default;
    ~A3()                           = default;

    bool f( T a ) { return g(a); }
};

オブジェクトをintにするときには、以下のように追加で記述する。

class F
{
  public:
    static bool f( int a ) { return ( 0 == a ); }
};

利用側では、こんな感じ。

  A3<int, F> a3;
  bool b0 = a3.f( 0 ); // true
  bool b1 = a3.f( 1 ); // false

最適化で、g( int a )はインライン展開されて消えるので、実質f(T a)からF::f(int a)を呼び出ししていることになる。
class Fは、スタティックな関数だけで構成されるので、コンストラクター等の記述は不要だ。普通のC++コンパイラコンパイルでそれらを生成しないだろう。
記述も最低必要な部分だけで済むので、メンテも簡単だ。
注意点として、上記のオブジェクトFはヘッダーで記述するか、利用するコードファイル内に配置しないと、オブジェクトの生成場所のコンパイルでエラーが発生する。

自分は少しだけ変形して使っている。

template <typename T>
class F
{
  public:
    static bool f( T a ) { return true; }
};

template <typename T, class C>
class A3
{
  private:
    bool g( T a ) { return C::f( a ); }
  public:
    A3() {}
    A3(            const A3&  lhs ) = default;
    A3(                  A3&& rhs ) = default;
    A3& operator=( const A3&  lhs ) = default;
    A3& operator=(       A3&& rhs ) = default;
    ~A3()                           = default;

    bool f( T a ) { return g(a); }
};

template <typename T>
using B3 = A3<T, F<T>>;

オブジェクトをintにするときには、以下のように追加で記述する。

template<>
class F<int>
{
  public:
    static bool f( int a ) { return ( 0 == a ); }
};

利用側では、こんな感じ。

  B3<int> b3;
  bool b0 = b3.f( 0 ); // true
  bool b1 = b3.f( 1 ); // false

利用時に特化関数オブジェクトを宣言に含めるか含めないかの違い程度だが、利用側のコードとしてはオブジェクトの型だけに気を配れば良いのがメリットだ。

拡張メタファイルの描画精度

Windows上でベクター画像を取り扱うプログラムを作成する場合、サイズを変更しても劣化のないメタファイル・拡張メタファイル形式は一定のメリットがある。
特にクリップボード経由の場合は便利なことが多い。
一方、メタファイルを用いる場合、ビットマップとは異なり解像度を意識してプログラムを行う必要がある。

ここからは拡張メタファイルに関しての考察になる。
拡張メタファイルは内部のサイズとして0.01mm単位で管理をしている。
ただし、MoveToなどの描画で与える座標値は0.01mm単位ではなく、拡張メタファイルを作成するときに与えるデバイスコンテキストの解像度単位であることに注意が必要だ。
以下のコードでは、デスクトップ画面のデバイスコンテキストで拡張メタファイルを作成した場合の解像度を計算する。

  HWND hWnd = ::GetDesktopWindow(); // デスクトップ画面のウィンドウハンドル
  HDC  hDc  = ::GetDC( hWnd );      // デスクトップ画面のデバイスコンテキスト

  int nMmH  = ::GetDeviceCaps( hDc, HORZSIZE ); // デスクトップ画面のサイズ(mm)
  int nResH = ::GetDeviceCaps( hDc, HORZRES  ); // 横ピクセル数(ピクセル)

  // 1ピクセルのサイズ(mm/ピクセル)
  double d = static_cast<double>(nMmH)/static_cast<double>(nResH); 

手元のパソコンで行った場合、nMmH=527(mm)、nResH=1920(ピクセル)で、d≒0.27447(mm/ピクセル)となった。
メタファイル中で指定する場合もピクセル単位で描画命令を発行するので、この場合はおおよそ0.27447(mm/ピクセル)で位置が指定され、0.01mm単位で位置を指定できない。

では、論理座標設定を行って0.01mm単位で描画コマンドを発行するようにするとどうなるか。
100mm×100mmのサイズで、精度0.01mmの拡張メタファイルを作りたいとする。

  int nWidth  = 10000; // 100mm
  int nHeight = 10000; // 100mm

  HWND hWnd = ::GetDesktopWindow(); // デスクトップ画面のウィンドウハンドル
  HDC  hDc  = ::GetDC( hWnd );      // デスクトップ画面のデバイスコンテキスト
  RECT R; 
  R.left   = 0;
  R.top    = 0; 
  R.right  = nWidth; 
  R.bottom = nHeight;
  HDC  hEmfDC = CreateEnhMetaFile( hDC, NULL, &R, NULL );
  
  double dPmmH = ::GetDeviceCaps( hEmfDc, HORZSIZE ) * 100.0; // 0.01mm単位
  double dResH = ::GetDeviceCaps( hEmfDc, HORZRES  );
  double dPmmV = ::GetDeviceCaps( hEmfDc, VERTSIZE ) * 100.0;
  double dResV = ::GetDeviceCaps( hEmfDc, VERTRES  );

  int nDevX = FWidth  * dResH / dPmmH;
  int nDevY = FHeight * dResV / dPmmV;

  ::SetMapMode(       hEmfDc, MM_ANISOTROPIC );
  ::SetWindowExtEx(   hEmf  , FWidth, FHeight, nullptr );
  ::SetViewportExtEx( hEmfDc, nDevX , nDevY  , nullptr );

確かに描画コマンドで0.01mm単位で指定できる。しかし内部でデバイス座標に変換するときにデバイス座標は整数で計算されるため、結局位置は上記の例だと0.27447単位に丸め込まれ、結果として位置がずれる。
例えば MoveTo(100,100)とすると、内部でのデバイス座標への変換で、100/27.447≒3.64となり、整数化で切り捨てが発生して実際のデバイス座標コマンドではMoveTo(3,3)が与えられる。元の0.01mmベースに戻すと、3*27.447≒82.341となり、意図した位置に描画されないことが分かるだろう。
つまり、この方法ではうまくいかない。

拡張メタファイルの生成時に与えるデバイスコンテキストが利用したい解像度になるよう調整できれば、簡単に解決できるが、Windowsの場合、アプリケーションプログラムで任意の解像度のデバイスコンテキストを作成できないはずなので、この方法は使えない。

解決するには、デバイス単位で描画し、その後求めるサイズにリサイズする。
VCLでの例を挙げる。以降では拡張メタファイルの廃棄のコードは記述していないことに注意。

  int nPmmWidth  = 10000; // 100mm
  int nPmmHeight = 10000; // 100mm

  HWND hWnd= ::GetDesktopWindow(); 
  HDC  hDc = ::GetDC( hWnd );

  TMetafile* mf = new TMetafile;
  mf->SetSize( nPmmWidth, nPmmHeight ); // ピクセルサイズ

  TMetafileCanvas* pMfCanvas = new TMetafileCanvas( mf, hDc );

  // 描画コマンド

  delete pCanvas;

  TMetafile* mf2 = new TMetafile;
  mf2->MMWidth  = nPmmWidth;  // 正しいサイズをセット
  mf2->MMHeight = nPmmHeight;

  double dPmmH = ::GetDeviceCaps( hEmfDc, HORZSIZE ) * 100.0;
  double dResH = ::GetDeviceCaps( hEmfDc, HORZRES  );
  double dPmmV = ::GetDeviceCaps( hEmfDc, VERTSIZE ) * 100.0;
  double dResV = ::GetDeviceCaps( hEmfDc, VERTRES  );

  int nDevX = nPmmWidth  * dResH / dPmmH;
  int nDevY = nPmmHeight * dResV / dPmmV;

  TMetafileCanvas* pMfCanvas2 = new TMetafileCanvas( mf2, hDc );
  pMfCanvas2->StretchDraw( TRect( 0, 0, nDevX, nDevY ), mf );
  delete pMfCanvas2;

Win32apiベースだと次のようなコードになる。
なお、以下のコードは動作検証をしていないので、間違いがあれば指摘して欲しい。

  int nPmmWidth  = 10000; // 100mm
  int nPmmHeight = 10000; // 100mm

  HWND hWnd= ::GetDesktopWindow(); 
  HDC  hDc = ::GetDC( hWnd );

  double dPmmH = ::GetDeviceCaps( hDc, HORZSIZE ) * 100.0;
  double dResH = ::GetDeviceCaps( hDc, HORZRES  );
  double dPmmV = ::GetDeviceCaps( hDc, VERTSIZE ) * 100.0;
  double dResV = ::GetDeviceCaps( hDc, VERTRES  );

  RECT R;
  R.left   = 0;
  R.top    = 0; 
  R.right  = nPmmWidth  * dPmmH / dResH; 
  R.bottom = nPmmHeight * dPmmH / dResH;

  HDC  hEmfDC = CreateEnhMetaFile( hDC, NULL, &R, NULL );

  // 描画コマンド

  HENHMETAFILE hEmf = CloseEnhMetaFile( hEmfDC );
  
  RECT R2;
  R2.left   = 0;
  R2.top    = 0; 
  R2.right  = nPmmWidth; 
  R2.bottom = nPmmHeight;

  HDC hEmfDC2 = CreateEnhMetaFile( hDC, NULL, &R2, NULL );

  RECT R3;
  R3.left   = 0;
  R3.top    = 0; 
  R3.right  = nPmmWidth  * dResH / dPmmH;
  R3.bottom = nPmmHeight * dResV / dPmmV;

  PlayEnhMetaFile( hEmfDC2, hEmf, &R3 );
  
  HENHMETAFILE hEmf2 = CloseEnhMetaFile( hEmfDC2 );

  // hEmf2を使って作業

C++ Builderの出力mapのアドレスは相対アドレス

タイトルの通り、C++Builderで出力されるmap中のアドレスは相対アドレスになる。
このため、実行時のアクセス違反メッセージで表示されるアドレス値とは異なることに注意。

ベースアドレスは最初に記述されている。例えば以下の通り。

 Start Length Name Class
 0001:00401000 0009FD8ECH _TEXT                  CODE
 0002:00DFF000 000063CB0H _DATA                  DATA
 0003:00E62CB0 000018FB0H _BSS                   BSS
 0004:00000000 0000000B4H _TLS                   TLS

コードの開始アドレスは、0001セグメントで0x00401000からだと分かる。コードの終了アドレスは0x0009FD8ECを足して0x00DFE8ECだ。
以降は関数とその相対アドレスが羅列されているので、例えば以下のように書かれていれば、0001セグメントの開始アドレスを足して絶対アドレスは0x0070477Cであることが分かる。

 0001:0030377C func()

逆に、アクセス違反メッセージで、例えば0x00704781などと表示されているのであれば、この関数内でエラーが起きている可能性が高いと分かる。
通常はアクセス違反メッセージから探すだろうから、0x00704781からベースアドレスを引いて、0x00303781以前の最も近い関数を探すことになる。

C++ Builderの出力mapを自動コピーして保存する

C++ Builderもリンク時にmapファイルを出力できる。
release版でアドレス違反が発生したときのメッセージをもとに、mapファイルで発生元の関数を推測できる。

ただし、そのためには準備がいる。
mapの出力設定はオプションのリンカ→出力→マップファイルの種類で「詳細なセグメントマップ」にする。
メイクあるいはビルド毎にmapファイルは上書きされ過去のmapファイルはなくなるので、最新版以外でトラブル発生に備えるならばmapの自動保存がされるように仕込んでおく必要がある。
自分がやっているのは、ビルドイベントとバッチファイルを組み合わせて、自動コピーする方法だ。
Gitなどを使っているのであれば、コミットすればmapもスナップショットが保存されるのかもしてない。そちらは自分で確認して欲しい。

まず、オプションのビルドイベントで、ビルド後のコマンドに以下を設定する。
設定するのは目的のターゲット(例えばWin32 release)のみでよいだろう。

chdir $(OUTPUTDIR)
map_copy.bat $(OUTPUTNAME)

mapファイルの保存先としてmapフォルダーを作成しておく(今回の例であればWin32\release\map)。
バッチファイル(map_copy.bat)はパスの通ったところに置く。
内容は以下の通り(2021-02-04修正)。

@echo off
set YYYY=%date:~0,4%
set MM=%date:~5,2%
set DD=%date:~8,2%
set H1=%time:~0,1%
set H2=%time:~1,1%
set NN=%time:~3,2%
if "%H1%" == " " set H1=0
copy %1.map map\%1_%YYYY%-%MM%-%DD%_%H1%%H2%-%NN%.map

これで例えばProject1のmapファイル(Project1.map)を2021-01-05 19:58に保存したとすれば、自動でmapフォルダーに名前(2021-01-05_19_58_Project1.map)をつけて保存してくれる。
この方法の場合、常にmapファイルのコピーが作成されるので、適当なタイミングで不要なmapファイルを削除した方が良いだろう。

2021-02-04 追記

時間が0~9時だとバッチファイルが正しく動作しない問題を修正した。
ifで%H1%に""をつけているのはそうしないと%H1%は空白に置き換えされてコマンドエラーを起こすため。比較対象の空白も""で囲む。
ついでに別解も示す。こちらの方がシンプルか。

@echo off
set mDate=%date:/=-%
set mTime=%time: =0%
set HH=%mTime:~0,2%
set MM=%mTime:~3,2%

copy %1.map map\%1_%mDate%_%HH%-%MM%.map

コマンドの詳細については、以下の記事を参考にした。
Windowsバッチまとめ - Qiita

フォームの閉じ方とModalResultの設定について

VCLでフォームを表示させ終了時にフラグで処理を変更したいときにフォームのModalResultを参照するケースがある。
ただし、ModalResultの設定については注意しなければいけない点があるのでメモ。
また、フォームの閉じ方についてもメモ。

ShowModalで表示させたフォームの閉じ方

ShowModal()でフォームを呼び出した場合、フォームを閉じるには2つの方法がある。
1.Close()関数を呼ぶ。フォームのModalResultにはmrCancelがセットされる。
2.ModalResultにmrNone以外をセットする。

2の方法はTButtonなどではそのModalResultプロパティをセットするとコードレスで閉じられる。セットされたオブジェクトのOnClickイベントに何か記述している場合は、そのコードの実行後、自動で閉じられる。
メニュー(TMenuItem)にはModalResultプロパティがないので、OnClickイベントでModalResultにセットする。

void __fastcall TForm1::MenuItem1Click( TObject* Sender)
{
  ModalResult = mrOk;
}

余計なお節介で、Close()を記述するとModalResultがmrCancelになってしまうのでModalResultのセットで閉じる場合には注意する。

void __fastcall TForm1::MenuItem1Click( TObject* Sender)
{
  ModalResult = mrOk;
  Close(); // ここでModalResultにmrCancelがセットされる。
}

自動生成を使わずにフォームを作成した場合は、フォームを閉じて終了処理を行った後は、フォームオブジェクトの廃棄をすること。
再突入でフォームオブジェクトの複数コピーが発生するのを避けるためだ。

bool show_form1( TForm* pParent )
{
  Form1 = new TForm1( pParent );

  // 初期化処理を記述

  TModalResult mrRet = Form1->ShowModal();
  
  // 終了時処理を記述

  delete Form1; // 忘れず記述

  return true;
}

std::unique_ptrを使う場合は、make_uniqueを使うとおかしな挙動を示す場合があるので、無駄があるがnewの代入で行う。
自分の経験ではTObjectの派生クラスでstdのmake~系を使うと動作が不安定になる場合があるので、VCLFMXフレームワークのオブジェクトの生成では注意が必要だ。
また、自動生成される定義を使わないことに注意。

#include <memory>
bool show_form1( TForm* pParent )
{
  std::uniqe_ptr<TForm1> pForm( new TForm1( pParent ) );

  // 初期化処理を記述

  TModalResult mrRet = pForm->ShowModal();
  
  // 終了時処理を記述

  //delete pForm; // 不要

  return true;
}

複数回突入が仮定され、フォームを使い回す場合は次のようなコードになる。

TForm1* Form1 = nullptr; // bcc32.exeを使う場合はnullptrの替わりにNULL
bool show_form1( TForm* pParent )
{
  if( !Form1 )
    Form1 = new TForm1( pParent );

  // 初期化処理を記述

  TModalResult mrRet = Form1->ShowModal();
  
  // 終了時処理を記述

  return true;
}

個人的には気持ちが悪いのでこの方法は使わない。

Showで表示させたフォームの閉じ方

まず、Showで表示させた場合、ModalResultをセットしても閉じられないことに注意。
そのためClose()関数を必ず呼ぶ必要がある。
自動生成を使わずにフォームを作成した場合は、フォームオブジェクトの廃棄コードをOnCloseイベントに記述する。
ヘルプで、何やらややこしいコードが示されているが、フォームオブジェクトの自動定義と初期化を使えば下記のコードで済む。

TForm1* Form1 = nullptr; // bcc32.exeを使う場合はnullptrの替わりにNULL
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
  // 終了時処理をここに記述

  Action = TCloseAction::caFree;
  Form1 = nullptr;
}

bool show_form1( TForm* pParent );
{
  if( Form1 == nullptr )
  {
    Form1 = new TForm1( pParent );
    // 初期化処理
  }

  Form1->Show();

  return true;
}

show_form1の戻り値では呼び出し元に処理結果を渡せないので、終了時処理で呼び出し元を呼び出す。
おすすめは中継関数を使って呼び出し元に値を返して、変更を反映させる方法だ。

// 呼び出し元 ParentForm.h
class TParentForm : public TForm
{
  //<中略>

  public:
    void update();
};
//---------------------------------------------------------------------------
extern PACKAGE TParentForm* ParentForm;
//---------------------------------------------------------------------------
#endif

// 中継関数定義ファイル CnParent.h
void parent_update();

// 中継関数定義ファイル CnParent.cpp
#include "ParentForm.h"
void parent_update()
{
  ParentForm->update();
}

// 呼び出し先 Form1.cpp
#include "CnParent.h"
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
  // 終了時処理をここに記述
  parent_update();

  Action = TCloseAction::caFree;
  Form1 = nullptr;
}

中継関数を別ファイルで定義して使うメリットは、フォーム間の依存関係を最小限にできることだ。
update()がパブリックなのが気になるのであれば、friendを使って可視性を制御する。