今回はFileNameCleanerの最終回として、FileNameCleanerProc.hの解説をします。

SkeltonWizardが生成するBCCSkeltonコードで、開発者が行いたい処理はほぼこの(プロジェクト名)Pron.hに書き込まれますので、このプログラムでも、ダイアログの二つのボタンを押したときの処理と共に、ここに次の処理を行うユーザー定義関数を書いています。

(1)ファイルパス、名の文字列にUnicodeの一般句読点符号(U+2000~U+206F)のコードがあるか否かをチェックし、あればこれを除去した文字列を生成する。→CheckName(WCHAR*);

(2)フォールダー内のファイルを列挙する。→CheckFiles(WCHAR*);

(3)オリジナルのファイルパス、名を除去後のファイルパス、名に変更する。→ShellRename(WCHAR*, WCHAR*);

なお、このプログラムは自動生成コード以外は全てワイド文字(WCHAR)で書いていますので、該当部分は色付けをします。

 

【FileNameCleanerProc.h】

//////////////////////////////////////////
// FileNameCleanerProc.h
// Copyright (c) 05/06/2020 by BCCSkelton
//////////////////////////////////////////

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////
bool CMyWnd::OnInit(WPARAM wParam, LPARAM lParam) {

    //クライアントエリアサイズを記録
    RECT rec;
    GetClientRect(m_hWnd, &rec);
    m_Width = rec.right - rec.left;
    m_Height = rec.bottom - rec.top;
    return TRUE;
}
//(解説:ダイアログサイズを変更した際にコントロールを移動させるための基礎となる、ダイアログ生成時のクライアントエリアサイズをメンバー変数に記録します。)


bool CMyWnd::OnSize(WPARAM wParam, LPARAM lParam) {

    //コントロールの位置、サイズ変更
    RECT rec;    //矩形取得用
    POINT pt;    //スクリーン座標変換用
    int w, h;    //幅、高さ計算用
    int diffx = LOWORD(lParam) - m_Width;    //前回と今回の差分
    int diffy = HIWORD(lParam) - m_Height;    //前回と今回の差分
    m_Width = LOWORD(lParam);                //今回の幅
    m_Height = HIWORD(lParam);                //今回の高さ
    //左右移動-IDC_SELECT
    GetWindowRect(GetDlgItem(m_hWnd, IDC_SELECT), &rec);    //ウィンドウ位置取得
    w = rec.right - rec.left;
    h = rec.bottom - rec.top;
    pt.x = rec.left;
    pt.y = rec.top;
    ScreenToClient(m_hWnd, &pt);
    MoveWindow(GetDlgItem(m_hWnd, IDC_SELECT), pt.x + diffx, pt.y, w, h, TRUE);
    //幅変更-IDC_EDIT
    GetWindowRect(GetDlgItem(m_hWnd, IDC_EDIT), &rec);    //ウィンドウ位置取得
    w = rec.right - rec.left;
    h = rec.bottom - rec.top;
    pt.x = rec.left;
    pt.y = rec.top;
    ScreenToClient(m_hWnd, &pt);
    MoveWindow(GetDlgItem(m_hWnd, IDC_EDIT), pt.x, pt.y, w + diffx, h, TRUE);
    //幅高さ変更-IDC_FILELIST
    GetWindowRect(GetDlgItem(m_hWnd, IDC_FILELIST), &rec);    //ウィンドウ位置取得
    w = rec.right - rec.left;
    h = rec.bottom - rec.top;
    pt.x = rec.left;
    pt.y = rec.top;
    ScreenToClient(m_hWnd, &pt);
    MoveWindow(GetDlgItem(m_hWnd, IDC_FILELIST), pt.x, pt.y, w + diffx, h + diffy, TRUE);
    //左右上下移動-IDOK
    GetWindowRect(GetDlgItem(m_hWnd, IDOK), &rec);    //ウィンドウ位置取得
    w = rec.right - rec.left;
    h = rec.bottom - rec.top;
    pt.x = rec.left;
    pt.y = rec.top;
    ScreenToClient(m_hWnd, &pt);
    MoveWindow(GetDlgItem(m_hWnd, IDOK), pt.x + diffx, pt.y + diffy, w, h, TRUE);
    //クライアントエリアを再描画
    InvalidateRect(m_hWnd, NULL, TRUE);
    return TRUE;
}
//(解説:ここがダイアログのサイズが変更された際(WM_SIZEメッセージ)に呼ばれる処理で、①最初のサイズと変更後のサイズの差分を取り、変更後のサイズを記録し、②次に縦横に移動したり、サイズ変更するコントロールのウィンドウサイズにその差分を反映して新しいウィンドウ位置、サイズに変更します。)

 

bool CMyWnd::OnClose(WPARAM wPram, LPARAM lParam) {

    //終了確認
    if(MessageBoxW(m_hWnd, L"終了しますか", L"終了確認",
                MB_YESNO | MB_ICONINFORMATION) == IDYES) {
    //処理をするとDestroyWindow、PostQuitMessageが呼ばれる
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnDestroy(WPARAM wPram, LPARAM lParam) {

    //ダイアログベースの場合はこれが必要
    PostQuitMessage(0);
    return TRUE;
}

//(解説:SkeltonWizardの作成したままの内容です。)


bool CMyWnd::OnMinMax(WPARAM wParam, LPARAM lParam) {

    //典型的なウィンドウのサイズ制限処理
    MINMAXINFO *pmmi;
    pmmi = (MINMAXINFO*)lParam;
    pmmi->ptMinTrackSize.x = MINW + 16;    //クライアントエリア + 16
    pmmi->ptMinTrackSize.y = MINH + 39;    //クライアントエリア + 61
    return FALSE;                            //処理はDefWndProcに任す
}

//(解説:サイズ変更時の最小サイズ処理です。)


/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//メニュー項目、コントロール関数
/////////////////////////////////
bool CMyWnd::OnSelect() {

    //ファイル名クリーニングを行うフォールダーの選択
    WCHAR* fp = g_Cmndlg.GetPath(m_hWnd,
                    L"ファイル名クリーニングを行うフォールダーの選択");

    //パス名を記録
    if(fp) {
        lstrcpyW(g_Path, fp);
        SendMessageW(GetDlgItem(m_hWnd, IDC_EDIT), WM_SETTEXT, 0, (LPARAM)g_Path);

    }
    else {
        MessageBoxW(m_hWnd, L"キャンセルされました", L"エラー",
                    MB_OK | MB_ICONERROR);

        return FALSE;
    }
    //フォールダー名のチェック
    if(CheckName(g_Path)) {
        MessageBoxW(m_hWnd, L"フォールダー名をチェックしました。\n"\
                    "続いてフォールダー内のファイルをチェックします。",
                    L"成功", MB_OK | MB_ICONINFORMATION);

        SendMessageW(GetDlgItem(m_hWnd, IDC_EDIT), WM_SETTEXT, 0, (LPARAM)g_Cleaned);
        lstrcpyW(g_Path, g_Cleaned);    //g_Pathに修正版を記録

    }
    else {
        MessageBoxW(m_hWnd, L"フォールダー名のクリーニングに失敗しました。",
                    L"失敗", MB_OK | MB_ICONERROR);

        return FALSE;
    }
    //ファイル名のチェック
    if(CheckFiles(g_Path)) {
        SendMessageW(GetDlgItem(m_hWnd, IDC_FILELIST), WM_SETTEXT, 0, (LPARAM)g_OriginalList.ToChar());
        MessageBoxW(m_hWnd, L"これらファイル名をチェックしました。\n"\
                    "最終ファイル名を表示します。",
                    L"成功", MB_OK | MB_ICONINFORMATION);
        SendMessageW(GetDlgItem(m_hWnd, IDC_FILELIST), WM_SETTEXT, 0, (LPARAM)g_CleanedList.ToChar());

        return TRUE;
    }
    else {
        MessageBoxW(m_hWnd, L"ファイル名のクリーニングに失敗しました。",
                    L"失敗", MB_OK | MB_ICONERROR);

        return FALSE;
    }
}

//(解説:クリーニング処理を行うフォールダーを「フォールダーを選ぶ」ダイアログで選択し、先ずフォールダー名をチェックし、一般句読点符号があればそれを除去し、次にフォールダー内のファイルをチェックして同様の処理をします。ワイド文字対応の部分は紫文字を見てください。'L'を付けた定数や'W'が付いた関数がワイド文字対応です。)


bool CMyWnd::OnOk() {

    SendMsg(WM_CLOSE, 0, 0);
    return TRUE;
}

///////////////////////////////
//ユーザー関数定義
//CheckName - フォールダー名、
//ファイル名に禁則文字が入って
//いるかチェックする
//無ければTRUEを返し、あれば
//削除結果を返す(TRUE、FALSE)
///////////////////////////////
bool CMyWnd::CheckName(WCHAR* name) {

    lstrcpyW(g_Original, name);
    //Unicodeの一般句読点(U+2000 - U+206F)がファイル名に含まれていれば、除去してRenameする
    int taboo = 0;
    WCHAR* sp = g_Original;
    WCHAR* dp = g_Cleaned;
    while(*sp != L'\0')
{
        if(*sp >= 0x2000 && *sp <= 0x206F) {    //(解説:WCHARはwchar_tのことで、実体はunsigned short integer(2バイト)です。)

            sp++;
            taboo++;
        }
        *dp++ = *sp++;    //g_Originalを修正してg_Cleanedへコピー
    }
    *dp = *sp;    //NULL終端をコピー
    WCHAR mes[MAX_PATH];
    if(taboo) {
        wsprintfW(mes, L"Unicodeの一般句読点(U+2000 - U+206F)文字が%d個見つかりました。削除しますか?", taboo);
        if(MessageBoxW(m_hWnd, mes, L"確認", MB_YESNO | MB_ICONQUESTION) == IDYES)
{
            if(!ShellRename(g_Original, g_Cleaned))
                return FALSE;
        }
        else {
            MessageBoxW(m_hWnd, L"Unicodeの一般句読点(U+2000 - U+206F)文字が残ったままです", L"警告",
                        MB_YESNO | MB_ICONQUESTION)
;
            return FALSE;
        }
    }
    return TRUE;
}

//(解説:外部変数のオリジナルファイルパス、名(g_Original)をWCHAR単位でチェックし、一般句読点符号があればその個数をtabooに記録し、それをスキップした文字列をg_Cleanedに記録します。そしてg_Originalのファイル名をg_Cleanedに変更します。)


///////////////////////////////
//ユーザー関数定義
//GetFileName - 1 フォールダーの
//ファイル名を列挙して、禁則文字
//が入っているかチェックする
///////////////////////////////
bool CMyWnd::CheckFiles(WCHAR* path) {

    //g_OriginalList、g_CleanedListの初期化
    g_OriginalList = L"";
    g_CleanedList = L"";

    //フォールダー内ファイルのチェック
    CSTRW fl(path);
    fl = fl + L"\\*.*";

    WIN32_FIND_DATAW fd;
    HANDLE hFind = FindFirstFileW(fl.ToChar(), &fd);

    if (hFind == INVALID_HANDLE_VALUE) {
        MessageBoxW(m_hWnd, L"ファイル情報取得失敗", L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    else {
        while(FindNextFileW(hFind, &fd)) {
            //フォールダの場合は何もしない
            if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
            //ファイルの場合、g_Originalにファイル名をコピーしてCheckNameに掛ける
            else {
                //オリジナルリストの作成
                g_OriginalList = g_OriginalList + path;
                g_OriginalList = g_OriginalList + L"\\";
                g_OriginalList = g_OriginalList + fd.cFileName;
                g_OriginalList = g_OriginalList + L"\r\n";

                //Unicodeの一般句読点除去と修正版リストの作成
                fl = path;
                fl = fl + L"\\";
                fl = fl + fd.cFileName;
                if(CheckName(fl.ToChar())) {
                    g_CleanedList = g_CleanedList + g_Cleaned;
                    g_CleanedList = g_CleanedList + L"\r\n";

                }
                else
                    MessageBoxW(m_hWnd, L"Unicodeの一般句読点除去に失敗しました", g_Original,
                                MB_OK | MB_ICONEXCLAMATION);

            }
        }
    }
    FindClose(hFind);
    return TRUE;
}

//(解説:指定フォールダー内のファイルを列挙し、一つづつファイル名チェックと一般句読点符号の除去を行います。)


//////////////////////////////////////////////
//ユーザー関数定義
//ShellExecuteExを起動し、プロセスの終了を待つ
//////////////////////////////////////////////
bool CMyWnd::ShellRename(WCHAR* original, WCHAR* clean) {

    //SHFileOperationを使う
    SHFILEOPSTRUCTW fop;
    ZeroMemory(&fop, sizeof(SHFILEOPSTRUCTW));
    fop.wFunc = FO_RENAME;
    //wFunc-実行したい機能を指定
    //FO_COPY    コピー
    //FO_DELETE    削除
    //FO_MOVE    移動
    //FO_RENAME    名前の変更
    fop.pFrom = original;
    fop.pTo   = clean;
    if(!SHFileOperationW(&fop)) {
        MessageBoxW(m_hWnd, L"SHFileOperationで名前変更しました", L"成功", MB_OK | MB_ICONINFORMATION);
        return TRUE;
    }
        else {
        MessageBoxW(m_hWnd, L"SHFileOperationの名前変更に失敗しました", L"失敗", MB_OK | MB_ICONERROR);
        return FALSE;
    }
}

//(解説:Shellサービスの「万能」ファイル処理関数です。これ一つあればExplorerの行うファイル処理は全てできますね。)

 

ということで、生まれて初めて(止むに止まれぬ事情から)Windowsが使うワイド文字(Unicode UTF-16)だけを使ってプログラムを書いてみました。前に書いたように次の点に気を付ければ、ASCIIベースで書くのと大して変わらずに書けますね。

(1)文字列は8バイトのchar型から16バイトWCHAR(wchar_t)型に替える。その為にwchar.hをインクルードする。
(2)今まで使っていた8バイト用のWin32API関数や構造体は(ワイド文字用のものが必ずあるので)ワイド文字用のものを使う。探す際のヒントは、以下の通りです。
 ①(関数名)→(関数名)W や"str"等の文字列を意味する文字が入っている関数名であれば"strW"に置き換えて検索する。
 ②(構造体名)→(構造体名)Wや①と同様に文字列を彷彿させる名称にWを付加したものを検索する。
(3)文字列定数は"なんちゃら"→L"なんちゃら"とする。単一文字の場合も'〇'→L'〇'とする。
(4)文字列のヌル終端はNULL(8バイト)でなく、明示的に16バイトのL'\0'とする。

 

しかし、このプログラムを書いて感じたことは、「確かにMicrosoft Visual C++(今のVisual Studio)を使う際に、簡単にASCIIベースと、ワイド文字(UTF-16、Microsoftはこれを"Unicode"というのですが...)ベースでスイッチできるTCHARを使って書くのは便利ですが、それによって『自分が今正確に何をしているのか分からなくなる』悪影響が出ると不味」だな、ということです。それでやっと前に引用したウェブの記事(TCHAR はもう使うな)の正しい意味が体感出来ました。

 

さて、それではもうUnicodeとは手を切って、自分の世界(「Unicodeには触るな!」)に戻ろう、と思いましたが、CSTRWのファイル操作対象がUTF-16だけ、というのが妙に引っかかってます。まぁ、PCでWindowsを使う限り、先ずUTF-7等の「はぐれUTF」や矢鱈メモリーを喰う「UTF-32」は無視してもよいので「リトルエンディアンのUTF-16+BOM」を基本とすることは問題ありませんが、現在Unicodeで主流となり、「どのエディター(Microsoftの「メモ帳」もWin10からUTF-8対応だそうです)でもUTF-8を読めるらしいのでBCCSkeltonだけ無視したら不味かろう」という想いと、Win32API(windows.h)にUTF-8も扱えるWideCharToMultiByte とMultiByteToWideCharという関数があることが分かりましたので、ちょっと機能拡張してみようかと思います。

 

では次回!

 

 

FileNameCleanerはSkeltonWizardが作るモードレスダイアログだけのシンプルなもので、選択フォールダーパス、フォールダー内のファイル列挙と一般句読点符号の除去処理だけですので、一挙に3ファイル紹介します。

 

【FileNameCleaner.rc】-ResFileNameCleaner.hは省略

//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------
#include    "ResFileNameCleaner.h"

//----------------------------------
// ダイアログ (IDD_MAIN)
//----------------------------------
IDD_MAIN DIALOG DISCARDABLE 0, 0, 320, 180
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_THICKFRAME | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_SETFONT | DS_CENTER
CAPTION "FileNameCleaner"
FONT 9, "MS 明朝"
{
 CONTROL "選択", IDC_SELECT, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 276, 2, 40, 14
 CONTROL "", IDC_EDIT, "EDIT", WS_CHILD | WS_VISIBLE | ES_READONLY | ES_LEFT | ES_AUTOHSCROLL, 4, 18, 312, 12, WS_EX_CLIENTEDGE
 CONTROL "終了", IDOK, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 276, 160, 40, 16
 CONTROL "", IDC_FILELIST, "EDIT", WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL| ES_LEFT, 4, 32, 312, 124, WS_EX_CLIENTEDGE
 CONTROL "フォールダー選択", IDC_LABEL, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 3, 3, 82, 14
}

//--------------------------
// イメージ(IDI_ICON)
//--------------------------
IDI_ICON    ICON    DISCARDABLE    "C:\Users\(パス)\Renamer\Icon.ico"
 

//(解説:基本EDITボックス2つ-リストボックスの様に見えますが-とボタン二つだけのシンプルなものです。WS_THICKFRAMEでダイアログサイズを変えられるようにしています。追記-後で気が付いたのですが、コピペしたのでアイコンはRenamerのフォールダーの物を使っていることになっていますね。これは"\FileNameCleaner\Icon.ico"の意図です。でもアイコンが同じで、現実に読み込めるのでエラーが出ずに気が付きませんでした...笑)

 

【FileNameCleaner.cpp】

//////////////////////////////////////////
// FileNameCleaner.cpp
//Copyright (c) 05/06/2020 by BCCSkelton
//////////////////////////////////////////
#include    <stdio.h>
#include    "FileNameCleaner.h"
#include    "FileNameCleanerProc.h"

////////////////
// WinMain関数
////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine, int nCmdShow) {

    //2重起動防止
    if(!FileNameCleaner.IsOnlyOne()) {
        //ここに2重起動時の処理を書く(下記は1例)
        HWND hWnd = FindWindow("MainWnd", "FileNameCleaner");
        if(IsIconic(hWnd))
            ShowWindow(hWnd, SW_RESTORE);
        SetForegroundWindow(hWnd);
        return 0L;
    }

    //モードレスダイアログを作成Create(hParent, DlgName, DlgProc);
    if(!FileNameCleaner.Create(NULL, hInstance, "IDD_MAIN", ModelessProc))
        return 0L;

    //メッセージループに入る
    return FileNameCleaner.Loop();
}
 

//(解説:SkeltonWizardが作ったままのコードをコメントアウトしただけです。)

 

【FileNameCleaner.h】

//////////////////////////////////////////
// FileNameCleaner.h
// Copyright (c) 05/06/2020 by BCCSkelton
//////////////////////////////////////////
//BCCSkeltonのヘッダー-これに必要なヘッダーが入っている
#include    "BCCSkelton.h"
//リソースIDのヘッダー
#include    "ResFileNameCleaner.h"
//ワイド文字を使用する
#include <wchar.h>
//ワイド文字対応wsprintfW使用の為
#include <winuser.h>
//ワイド文字対応StrStrW使用の為
#include <shlwapi.h>

//ワイド文字対応CSTRWクラス
#include "CSTRW.h"
//ワイド文字対応CMNDLGWクラス
#include "CMNDLGW.h"

//最小ウィンドウ幅、高さ初期値
#define        MINW    480
#define        MINH    270

//(解説:まずワイド文字使用の為に必要なヘッダーを読み込みます。Microsoft Docにワイド文字関連構造体や関数がどのヘッダーに規定されているか出ていますので確認してください。また、今回作ったばかりのCSTRWとCMNDLGWを使用します。)


/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCDLGクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////
class CMyWnd : public CDLG
{
public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(char* UName) : CDLG(UName) {}
    //メンバー変数
    int m_Width;        //ダイアログクライアントエリア幅
    int m_Height;        //ダイアログクライアントエリア高さ

//(解説:サイズ変更した際のコントロール再配置用です。)

    //メニュー項目、ダイアログコントロール関連
    bool OnSelect();
    bool OnOk();

//(解説:2つのボタンが押された場合の処理だけです。)

    //ウィンドウメッセージ関連
    bool OnInit(WPARAM, LPARAM);
    bool OnSize(WPARAM, LPARAM);
    bool OnClose(WPARAM, LPARAM);
    bool OnDestroy(WPARAM, LPARAM);
    bool OnMinMax(WPARAM, LPARAM);

//(解説:最小サイズ確保処理用です。)

    //ユーザー関数関連
    bool CheckName(WCHAR*);
    bool CheckFiles(WCHAR*);
    bool ShellRename(WCHAR*, WCHAR*);

//(解説:順にファイルパス・名に一般句読点符号があるか否か、フォールダー内のファイルのチェック、およびSHFileOperationWを使ったファイル名変更処理の関数です。SHFileOperation以外のMoveFileW等を使っても構いませんが、今回これを初めて使ってみました。)

};

////////////////////////////////////////////////////////////////////////
//派生させたCMyWndクラスのインスタンスとコールバック関数(マクロ)の作成
//主ウィンドウはダイアログと違い、コールバック関数は一つしか作れない
////////////////////////////////////////////////////////////////////////
CMyWnd FileNameCleaner("FileNameCleaner");    //ウィンドウクラスインスタンスの生成

BEGIN_MODELESSDLGMSG(ModelessProc, FileNameCleaner)    //コールバック関数名は主ウィンドウの場合ModelessProcにしている
    //メニュー項目、ダイアログコントロール関連
    ON_COMMAND(FileNameCleaner, IDC_SELECT, OnSelect())
    ON_COMMAND(FileNameCleaner, IDOK, OnOk())
    //自動的にダイアログ作成時にOnInit()、終了時にOnClose()を呼びます
    ON_SIZE(FileNameCleaner)
    ON_DESTROY(FileNameCleaner)
    ON_(FileNameCleaner, WM_GETMINMAXINFO, OnMinMax(wParam, lParam))

//(解説:最小サイズ確保処理用です。)

END_DLGMSG

////////////////////////
//コモンダイアログの作成
////////////////////////
CMNDLGW g_Cmndlg;

//(解説:いつものCMNDLGではなくCMNDLGWクラスです。)


//////////////////////////////
//フォールダー、ファイル名関連
//////////////////////////////
WCHAR g_Path[MAX_PATH];
WCHAR g_Original[MAX_PATH];
WCHAR g_Cleaned[MAX_PATH];
CSTRW g_OriginalList;
CSTRW g_CleanedList;

//(解説:いつものcharではなくWCHAR、CSTRではなくCSTRWクラスです。)

 

きわめてあっさりしていますね。またBCCSkeltonだとこのようなツールづくりがすごく簡単にできます。

次回はFileNameCleanerProc.hを紹介しましょう。

 

【バグは続くよ、どこまでも】シリーズから始まったUnicode LRM(U+E200)←→SJIS変換のゴミ('?')対策は、基本的にASCII "char"ベースのBCCSkeltonクラスを、Windowsが内部的に使用するUTF-16の"WCHAR"ベースに書き直したBCCSkelton-WCVによって解決されることになりました。(注)

注:「元々WindowsがUTF-16ベースなんだから、ファイルパス、名もUnicodeで書けば問題ないんじゃない?」というご主張もあるかと思いますが、「ファイルパス、名に、必要性が無ければ余計なコントロールコードを入れない」という方針は正しいし、そのコントロールコードがコード変換の際に不可逆性を有するなら、それを除去することは正しいと今でも思っています。特にこの「U+E200が混入」した原因が、意図せざる日付データのコピーによるものであり、更に真であると考えます。

 

当初は「禁則文字'?'の混入-バグ」と理解していましたが、結局はUTF-16コードがSJISへ変換される際に、対応文字が無い場合'?'に変換されるという仕様であると考えられ、FileNameCleanerの新しい仕様は「Unicodeの一般句読点記号(U+2000~U+206F)の、ファイルパス、名への意図せざる混入の発見、除去」としました。

 

先ず、チェック対象のフォールダーを「選択」ボタンで特定します。するとファイルパスがチェックされ、Unicodeの一般句読点記号があるか否か、いくつあるか教えてくれます。またEDITボックスにそのファイルパスが表示されます。(注)

注:EDITボックスにSendMessageW関数でUTF-16文字列を送った際の表示ですが、SJIS変換されているようです。

「はい」ボタンを押すとフォールダーのファイルパスからUnicodeの一般句読点記号が除去され、次にこのフォールダー内のファイルをチェックします。(変換直後はまだ'?'が残っています。)

同様にファイルの中のUnicodeの一般句読点記号がチェックされ、「はい」ボタンで除去されます。(この段階ではフォールダー名が更新され、'?'が除去されています。)

最後の(このサンプルでは3つ目の)ファイルで、SHFileOperationのエラーメッセージが出ます。何故か名前変更ファイル名にワイルドカードが使われたような反応ですが、それはない筈です。「スキップ」ボタンを押してみます。

Rename処理されたファイルのオリジナルファイル名がリストされます。('?'がありますね。)

最終ファイル名の表示を行うと全てのファイルから'?'が除去されており、別途Explorerで調べても全て除去されています。(従って先ほどのエラーメッセージと「スキップ」は何をスキップしたのかよく分かりませんね。→一応コードを見たら、do {~} while();ループを使っていたので、while() {}に替えたらエラーメッセージダイアログは消えました。)

 

ということで、所期のプログラムの完成です。

 

Unicodeを学習しながら書いているので、なかなか難しいものがありますが、現時点でのCSTRクラスのワイド文字版(Windowsは2バイト文字WCHARを使用するのでUTF-16)を紹介します。

 

前回も書きましたが、最も基礎的な変更点は以下の通り。

(1)文字列は8バイトのchar型から16バイトWCHAR(wchar_t)型に替える。その為にwchar.hをインクルードする。
(2)今まで使っていた8バイト用のWin32API関数や構造体は(ワイド文字用のものが必ずあるので)ワイド文字用のものを使う。探す際のヒントは、以下の通りです。
 ①(関数名)→(関数名)W や"str"等の文字列を意味する文字が入っている関数名であれば"strW"に置き換えて検索する。
 ②(構造体名)→(構造体名)Wや①と同様に文字列を彷彿させる名称にWを付加したものを検索する。
(3)文字列定数は"なんちゃら"→L"なんちゃら"とする。単一文字の場合も'〇'→L'〇'とする。
(4)文字列のヌル終端はNULL(8バイト)でなく、明示的に16バイトのL'\0'とする。

しかし、CSTRはSJISの文字列を扱い、ファイル読書きもするので更に次の点が要点になります。

(5)SJISでは全角文字が2バイトでShift-JISの第1バイト(0x81-0x9Fまたは0xE0-0xEF)があれば例外処理をしていましたが、それがほぼ(注1)不要となります。

(6)ファイルはWCHAR(UTF-16)ベースでの読書きとなります。(注2)ファイルを書く際には必ずBOMが必要となります。また読む際にはBOMを確認して、(CSTRが文字列テキストであろうが何だろうが一応読み込むことから)UTF-16テキストであればBOM以降を、それ以外であれば全てを読むようにしました。(従ってUTF-32のテキストではBOMの0xFF、0xFEの次にいきなりL'\0'(2バイトNULL)が来ることになります。)

注1:サロゲートペア文字はプログラミング対象テキストではめったに出てこないので...

注2:少なくともUTF-8の読み書きもしたいな、とも考えましたが、テキストコンバージョンはCSTRの仕様外ですし、結構めんどいので今回はストレートにUTF-16だけにしています。しかし、コーディング上はUTF-32、UTF-8の読み込みの可能性を意識していますので参考にしてください。また、現実のUTF-8、UTF-16の変換を実装される場合はこの記事が大変参考になります。(経緯や評価で、前回の【無駄話】が間違っていなかったことを示していますね。)

 

以下は上記(1)~(4)は解説を省略し、(5)、(6)のみ触れます。

 

【CSTRW.h】

//-------------------------
//         CSTRW.h
//   Copyright April 2022
//     By K. Yoshida
//      Version 1.0A
//注:使用にはwchar.hが必要
//-------------------------
#ifndef    _CSTRW_
#define _CSTRW_

class CSTRW {

    WCHAR*    m_str;

public:
    CSTRW();                                    //コンストラクター(無)
    CSTRW(const WCHAR*);                        //コンストラクター(文字列)
    CSTRW(const CSTRW&);                        //コンストラクター(CSTRW)
    CSTRW(const long);                            //コンストラクター(整数)
    ~CSTRW();                                    //デストラクター
    CSTRW& operator = (const CSTRW&);            //operator = (B) ---> = B;
    CSTRW& operator = (const long);                //operator = (B) ---> = B;
    WCHAR* ToChar() {return m_str;}                //文字列ポインターとして返す
    void Print();                                //m_strを印字する
    friend CSTRW operator + (const CSTRW&, const CSTRW&);    //A operator + (B)  --->= A + B;
    friend bool operator == (const CSTRW&, const CSTRW&);    //A operator == (B) --->= A == B;
    friend bool operator == (const CSTRW&, const WCHAR*);    //A operator == (B) --->= A == B;
    friend bool operator != (const CSTRW&, const CSTRW&);    //A operator == (B) --->= A != B;
    friend bool operator != (const CSTRW&, const WCHAR*);    //A operator == (B) --->= A != B;
    bool FromFile(WCHAR*, WCHAR*, HWND);        //ファイルからの読込
    bool FromFile(WCHAR*);                        //ファイルからの読込
    bool ToFile(WCHAR* , WCHAR*, HWND);            //ファイルへの書き込み
    bool ToFile(WCHAR*);                        //ファイルへの書き込み
    bool Next(CSTRW&);                            //m_strの単語を切り取り出す
    bool GetLine(CSTRW&);                        //この行末まで進む
    bool NextLine();                            //次の行頭まで進む
    WCHAR* Find(WCHAR*, WCHAR*);                //文字列の検索
};

//コンストラクター(無)
CSTRW::CSTRW() {

    m_str = new WCHAR[1];
    *m_str = L'\0';
}

//コンストラクター(文字列)
CSTRW::CSTRW(const WCHAR* string) {

    m_str = new WCHAR[lstrlenW(string) + 1];
    lstrcpyW(m_str, string);
}

//コンストラクター(CSTRW)
CSTRW::CSTRW(const CSTRW& cst) {

    m_str = new WCHAR[lstrlenW(cst.m_str) + 1];
    lstrcpyW(m_str, cst.m_str);
}

//コンストラクター(整数)
CSTRW::CSTRW(const long i) {

    WCHAR sbuff[MAX_PATH / 8];
    wsprintfW(sbuff, L"%d", i);
    m_str = new WCHAR[lstrlenW(sbuff) + 1];
    lstrcpyW(m_str, sbuff);
}

//デストラクター
CSTRW::~CSTRW() {

    delete [] m_str;
}

//CSTRWの代入
CSTRW& CSTRW::operator = (const CSTRW& cst) {

    if(&cst != this) {    //You can't copy yourself
        delete [] m_str;
        m_str = new WCHAR[lstrlenW(cst.m_str) + 1];
        lstrcpyW(m_str, cst.m_str);
    }
    return *this;
}

//整数の代入
CSTRW& CSTRW::operator = (const long i) {

    WCHAR sbuff[MAX_PATH / 8];
    wsprintfW(sbuff, L"%d", i);
    m_str = new WCHAR[lstrlenW(sbuff) + 1];
    lstrcpyW(m_str, sbuff);
    return *this;
}

//m_strを印字する
void CSTRW::Print() {

    MessageBoxW(NULL, m_str, L"CSTRW Message", MB_OK);
}

//追加演算子の定義
CSTRW operator + (const CSTRW& cst1, const CSTRW& cst2) {

    CSTRW strw;
    delete [] strw.m_str;
    strw.m_str = new WCHAR[lstrlenW(cst1.m_str) + lstrlenW(cst2.m_str) + 1];
    lstrcpyW(strw.m_str, cst1.m_str);
    lstrcatW(strw.m_str, cst2.m_str);
    return strw;
}

//比較演算子の定義(1)
bool operator == (const CSTRW& cst1, const CSTRW& cst2) {

    return !lstrcmpW(cst1.m_str, cst2.m_str);
}

//比較演算子の定義(2)
bool operator == (const CSTRW& cst, const WCHAR* str) {

    return !lstrcmpW(cst.m_str, str);
}

//比較演算子の定義(3)
bool operator != (const CSTRW& cst1, const CSTRW& cst2) {

    return lstrcmpW(cst1.m_str, cst2.m_str);
}

//比較演算子の定義(4)
bool operator != (const CSTRW& cst, const WCHAR* str) {

    return lstrcmpW(cst.m_str, str);
}

//ファイルからの読込
bool CSTRW::FromFile(WCHAR* Filter, WCHAR* FilePath, HWND hWnd = NULL) {

    WCHAR FileName[MAX_PATH];
    WCHAR file_path[MAX_PATH];
    lstrcpyW(file_path, FilePath);    //Default Path
    OPENFILENAMEW ofn;                //オープンファイルダイアログ構造体
    //オープンファイルダイアログでファイル名を取得する
    FileName[0] = NULL;
    ZeroMemory(&ofn, sizeof(ofn));    //ゼロ初期化
    ofn.lStructSize        = sizeof(ofn);
    ofn.hwndOwner        = hWnd;
    ofn.lpstrFilter        = Filter;
    ofn.nFilterIndex    = 1;
    ofn.lpstrFile        = FileName;
    ofn.nMaxFile        = MAX_PATH;
    if(!*FilePath)                    //規定値のNULLであれば現在のディレクトリー
        GetCurrentDirectoryW(MAX_PATH, file_path);
    else
        lstrcpyW(file_path, FilePath);
    ofn.lpstrInitialDir = file_path;
    ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
    if(!GetOpenFileNameW(&ofn))    return FALSE;        //読み込み結果
    //ファイルを読み込む
    BOOL bSuccess = FALSE;
    HANDLE hFile;
    hFile = CreateFileW(FileName, GENERIC_READ, FILE_SHARE_READ, NULL,
                        OPEN_EXISTING, NULL, NULL);    //ファイルを読込みでオープン
    if(hFile != INVALID_HANDLE_VALUE) {                //オープンできたか
        DWORD dwFileSize = GetFileSize(hFile, NULL);
        if(dwFileSize != 0xFFFFFFFF) {                //-1は(0xFFFFFFFF)エラー
            LPBYTE Buff;                            //読み込みバッファ用ポインター
            Buff = new BYTE[dwFileSize];
//(解説:直接WCHARのm_strに読み込ませず、一旦バイト単位のバッファ―に読み込ませます。)

            DWORD dwRead;
            if(ReadFile(hFile, Buff, dwFileSize, &dwRead, NULL)) {
                bSuccess = TRUE; //読み込み成功
            }
            if(Buff[0] == 0xFF && Buff[1] == 0xFE) {//UTF-16、UTF-32のBOM

//(解説:ここでまず頭の2バイトをチェックし、UTF-16とUTF-32が使う'FF FE'があるか否か、確認します。)

               if(Buff[2] & Buff[3]) {                //いずれもNULLではない

//(解説:更にリトルエンディアンの2バイトの'0 0'がなく、UTF-32でないことを確認します。)

                    delete [] m_str;                //元の文字列を廃棄して
                    //BOM以降の読み込んだデータを入れられるようにする
                    m_str = new WCHAR[dwFileSize / sizeof(WCHAR)];

//(解説:CSTR版では終端用の+1がありますが、BOMを差し引くとこうなります。)

                    CopyMemory(m_str, Buff + 2, dwFileSize - 2);

//(解説:BYTE配列をWCHAR配列へ(両方がオーバーラップしていないので)コピーします。)

                    //念のため、バッファの終端にNULLを置く
                    m_str[(dwFileSize - 2) / sizeof(WCHAR) - 1] = L'\0';
                }
                //else {    //UTF-32の処理

//(解説:UTF-32の場合にはここに変換処理を書きます。)

                //}            }
            //else if(Buff[0] == 0xEF && Buff[1] == 0x0xBB && Buff[1] == 0xBF) {    //UTF-8の処理

//(解説:UTF-8の場合にはここに変換処理を書きます。)

            //}
            else {                                    //UTF-16(32、8)ではない場合

//(解説:それ以外の場合は「丸のみ」します。)

                m_str = new WCHAR[dwFileSize / sizeof(WCHAR) + 1];
                CopyMemory(m_str, Buff, dwFileSize);
                //念のため、バッファの終端にNULLを置く
                m_str[dwFileSize / sizeof(WCHAR)] = L'\0';
            }
            delete [] Buff;                            //読み込みバッファ解放
        }
        CloseHandle(hFile);                            //ファイルのクローズ
    }
    return bSuccess;
}

//ファイルからの読込
bool CSTRW::FromFile(WCHAR* FileName) {

    BOOL bSuccess = FALSE;
    HANDLE hFile;
    hFile = CreateFileW(FileName, GENERIC_READ, FILE_SHARE_READ, NULL,
                        OPEN_EXISTING, NULL, NULL);    //ファイルを読込みでオープン
    if(hFile != INVALID_HANDLE_VALUE) {                //オープンできたか
        DWORD dwFileSize = GetFileSize(hFile, NULL);
        if(dwFileSize != 0xFFFFFFFF) {                //-1は(0xFFFFFFFF)エラー
            LPBYTE Buff;                            //読み込みバッファ用ポインター
            Buff = new BYTE[dwFileSize];
            DWORD dwRead;
            if(ReadFile(hFile, Buff, dwFileSize, &dwRead, NULL)) {
                bSuccess = TRUE; //読み込み成功
            }
            if(Buff[0] == 0xFF && Buff[1] == 0xFE) {//UTF-16、UTF-32のBOM
                if(Buff[2] & Buff[3]) {                //いずれもNULLではない
                    delete [] m_str;                //元の文字列を廃棄して
                    //BOM以降の読み込んだデータを入れられるようにする
                    m_str = new WCHAR[dwFileSize / sizeof(WCHAR)];
                    CopyMemory(m_str, Buff + 2, dwFileSize - 2);
                    //念のため、バッファの終端にNULLを置く
                    m_str[(dwFileSize - 2) / sizeof(WCHAR) - 1] = L'\0';
                }
                //else {    //UTF-32の処理
                //}
            }
            //else if(Buff[0] == 0xEF && Buff[1] == 0x0xBB && Buff[1] == 0xBF) {    //UTF-8の処理
            //}
            else {                                    //UTF-16(32、8)ではない場合
                m_str = new WCHAR[dwFileSize / sizeof(WCHAR) + 1];
                CopyMemory(m_str, Buff, dwFileSize);
                //念のため、バッファの終端にNULLを置く
                m_str[dwFileSize / sizeof(WCHAR)] = L'\0';
            }
            delete [] Buff;                            //読み込みバッファ解放
        }
        CloseHandle(hFile);                            //ファイルのクローズ
    }
    return bSuccess;
}

//(解説:同じ処理です。)


//ファイルへの書き込み(規定値UTF-16 リトルエンディアン)
bool CSTRW::ToFile(WCHAR* Filter, WCHAR* DefExt, HWND hWnd = NULL) {

    WCHAR FileName[MAX_PATH];
    WCHAR file_path[MAX_PATH];
    OPENFILENAMEW ofn;                //オープンファイルダイアログ構造体
    //オープンファイルダイアログでファイル名を取得する
    ZeroMemory(&ofn, sizeof(ofn));    //ゼロ初期化
    FileName[0] = NULL;
    ofn.lStructSize        = sizeof(ofn);
    ofn.hwndOwner        = hWnd;
    ofn.lpstrFilter        = Filter;
    ofn.nFilterIndex    = 1;
    ofn.lpstrFile        = FileName;
    ofn.nMaxFile        = MAX_PATH;
    ofn.lpstrInitialDir = L".";
    ofn.lpstrDefExt        = DefExt;    //デフォルトの拡張子
    ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_HIDEREADONLY |
                OFN_OVERWRITEPROMPT;
    if(!GetSaveFileNameW(&ofn))    return FALSE;;
    //ファイルを書き込む
    BOOL bSuccess = FALSE;
    HANDLE hFile;
    hFile = CreateFileW(FileName, GENERIC_WRITE, NULL, NULL,    //ファイルを書込みでオープン
                        CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if(hFile != INVALID_HANDLE_VALUE) {        //オープンできたか
        //バッファから文字列長取得し、NULL終端も加算
        DWORD dwTextLength = (lstrlenW(m_str) + 1) * sizeof(WCHAR);
        DWORD dwWritten;
        //BOMの書き込み - WindowsはIntel系でUTF-16 FF FE(リトルエンディアン)
        BYTE BOM[2] = {0xFF, 0xFE};
        if(WriteFile(hFile, BOM, 2, &dwWritten, NULL)) {

//(解説:まずUTF-16のBOMを書き込みます。)

            WriteFile(hFile, m_str, dwTextLength, &dwWritten, NULL);
            bSuccess = TRUE;    //書込み成功
        }
        CloseHandle(hFile);            //ファイルのクローズ
    }
    return bSuccess;
}

//ファイルへの書き込み(規定値UTF-16 リトルエンディアン)
bool CSTRW::ToFile(WCHAR* FileName) {

    BOOL bSuccess = FALSE;
    HANDLE hFile;
    hFile = CreateFileW(FileName, GENERIC_WRITE, NULL, NULL,    //ファイルを書込みでオープン
                        CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if(hFile != INVALID_HANDLE_VALUE) {        //オープンできたか
        //バッファから文字列長取得し、NULL終端も加算
        DWORD dwTextLength = (lstrlenW(m_str) + 1) * sizeof(WCHAR);
        DWORD dwWritten;
        //BOMの書き込み - WindowsはIntel系でUTF-16 FF FE(リトルエンディアン)
        BYTE BOM[2] = {0xFF, 0xFE};
        if(WriteFile(hFile, BOM, 2, &dwWritten, NULL)) {
            WriteFile(hFile, m_str, dwTextLength, &dwWritten, NULL);
            bSuccess = TRUE;    //書込み成功
        }
        CloseHandle(hFile);            //ファイルのクローズ
    }
    return bSuccess;
}

//(解説:同じ処理です。)


//m_strの単語を切り取り出す
bool CSTRW::Next(CSTRW& destination) {

    CSTRW str;
    delete [] str.m_str;
    str.m_str = new WCHAR[lstrlenW(m_str) + 1];
    WCHAR* ptr = m_str;
    WCHAR* des = str.m_str;
    bool Terminate = TRUE;
    //スペース文字はスキップする
    while(iswspace(*ptr) || *ptr == L' ' || *ptr == L',') {

//(解説:L' 'というのは全角スペースです。CSTRの時はここの処理が2バイトチェックでした。)

            ptr++;                        //スキップスペース文字
    }
    //非スペース文字を調べる
    while(!iswspace(*ptr)) {            //スペース文字でない
        if(!*ptr) {                        //NULL終端ならFALSEを返す
            Terminate = FALSE;
            break;
        }
        else if(*ptr == L' ') {
            break;                        //全角" "なら終了
        }
        else if(*ptr == L',') {
            ptr++;                        //','をスキップして次の文字まで進む
            break;                        //空白文字扱い
        }
        else if(*ptr == L'"') {
            ptr++;                        //'"'をスキップして中身だけ取り出す
            while(*ptr != L'"') {        //'"'なら'"'まで進む
                if(!*ptr) {                //NULL終端ならFALSEを返す
                    Terminate = FALSE;
                    break;
                }
                *des = *ptr;
                ptr++;    des++;
            }
            ptr++;                        //ソースポインターは次の文字へ進める
            break;                        //ループを抜けてデスティネーションはNULLが入る
        }
        else if(*ptr == L'/' && *(ptr + 1) == L'*') {
            ptr++;    ptr++;                //"/*~*/"コメント
            while(!(*ptr == L'*' && *(ptr + 1) == L'/')) {
                if(!*ptr) {                //NULL終端ならFALSEを返す
                    Terminate = FALSE;
                    break;
                }
                ptr++;
            }
            ptr++;    ptr++;
            //再度スペース文字をスキップする
            while(iswspace(*ptr) || *ptr == L' ' || *ptr == L',') {
                if(!*ptr) {                //NULL終端ならFALSEを返す
                    Terminate = FALSE;
                    break;
                }
                ptr++;                //スキップスペース文字
            }
            continue;
        }
        else if(*ptr == L'/' && *(ptr + 1) == L'/') {
            while(*ptr != L'\n') {        //"//"コメント
                if(!*ptr) {                //NULL終端ならFALSEを返す
                    Terminate = FALSE;
                    break;
                }
                ptr++;
            }
            while(iswspace(*ptr) || *ptr == L' ' || *ptr == L',') {
                if(!*ptr) {                //NULL終端ならFALSEを返す
                    Terminate = FALSE;
                    break;
                }
                ptr++;                //スキップスペース文字
            }
            continue;
        }
        else {
            *des = *ptr;
        }
        ptr++;    des++;
    }
    *des = L'\0';

    destination = str;            //destinationに切り取った文字列を代入
    str = ptr;                    //strにm_strより進んだptr以降の文字列を代入
    delete [] m_str;
    m_str = new WCHAR[lstrlenW(str.m_str) + 1];
    lstrcpyW(m_str, str.m_str);    //m_strには切り取った後の文字列が入る
    if(*str.m_str || *destination.m_str)
        Terminate = TRUE;        //残がNULLだけでも切り取った文字列があれば続ける
    else
        Terminate = FALSE;        //NULLだけなら終わり
    return Terminate;            //成功・失敗フラグを返す
}

//この行末まで進む
bool CSTRW::GetLine(CSTRW& destination) {

    bool Terminate = TRUE;
    WCHAR* ptr = m_str;
    WCHAR* des = ptr;
    while(*des && *des != L'\n')
        des++;
    if(!*des) {
        destination = L"";
        Terminate = FALSE;    //文字列の終了サイン
    }
    else {
        *des = L'\0'; des++;                //NULL終端させ、一つ進める
        delete [] destination.m_str;
        destination.m_str = new WCHAR[des - ptr + 1];
        lstrcpyW(destination.m_str, ptr);
        destination.m_str[des - ptr - 1] = L'\n';    //NULLにした'\n'を付加
        destination.m_str[des - ptr] = L'\0';        //NULL終端
    }
    if(Terminate) {                    //まだ文字列があれば残りを入れる
        CSTRW str = des;
        delete [] m_str;
        m_str = new WCHAR[lstrlenW(str.m_str) + 1];
        lstrcpyW(m_str, str.m_str);    //m_strには切り取った後の文字列が入る
    }
    if(*destination.m_str)            //NULLに出会ったが、
        Terminate = TRUE;            //切り取った文字列があれば続ける
    else
        Terminate = FALSE;            //NULLだけなら終わり
    return Terminate;                //成功・失敗フラグを返す
}

//次の行頭まで進む
bool CSTRW::NextLine() {

    WCHAR* ptr = m_str;
    while(*ptr != L'\n') {        //改行コードまで進む
        if(!*ptr) {                //途中でNULL終端したならFALSEを返す
            return FALSE;
        }
        ptr++;
    }
    ptr++;
    CSTRW str = ptr;
    delete [] m_str;
    m_str = new WCHAR[lstrlenW(str.m_str) + 1];
    lstrcpyW(m_str, str.m_str);    //m_strには切り取った後の文字列が入る
    return TRUE;                //成功・失敗フラグを返す
}

//文字列の検索
WCHAR* CSTRW::Find(WCHAR* ToFind, WCHAR* sp = 0) {

    if(!sp)
        sp = m_str;
    return StrStrW(sp, ToFind);
}

//(解説:実はここのコードをCSTRも変更しました。一回目の検索の際の戻り値+1を第二引数に入れて二回目以降の検索を行います。)

#endif

 

まぁ、実際には(無知の為に)2度ほどファイル読書きの部分を書き直しましたし、一旦UTF-8ファイルの読み込みのみをコーディングしましたが、矢張り「中途半端はやめよう」ということで、飽くまでも「WCHARを使ったCSTRのワイド文字版」に徹しています。

【UTF-16読込】

ToFileで書きだしたものを読み込んでいます。

【UTF-8読込】

当たり前ですが、「丸のみ」なので文字化けします。

 

(以下は私の勝手な解釈で、必ずしも事実や確立された解釈とは同一ではないかもしれません。)

 

「Unicodeには触るな」というのが20年前の私のモットーだった。(仕事は北米、東南アジア駐在を含め、国際的だったが)別に趣味のプログラミングで「国際標準に準拠」しなくても、アラビア、ハングルやタイ文字で使えなくてもよいから、「ASCII(American Standard Code for Information Interchange)と英語をしっかり押さえ」さえすれば、日本語はShift-JISでよいのだ、と思っていた。

 

また"Uni-"(「ユニ」)という言葉への不信もあったかもしれない。第一次世界大戦後国際連盟(League of Nations)が設立され、無力で独伊日が脱退して第二次世界大戦となった後設立された国連(United Nations)も、私が小学生の時(核戦争の現実の脅威があった冷戦時代)から「所期の目的通り機能していない」と教わり、現状も現在のユークライナを見ればわかる。こういう(独善的ともいえる)高邁な統一の理想や統合への志向は、その現実とのギャップから私に不信感を植え付けたのかもしれない。

 

Unicodeというと。「あーあれでしょ?世界中の言語を纏めて統一した符号体系じゃないの?今のコンピューターってそれ使っているんでしょ?」という認識が一般的ではないか?(事実、「'?'禁則文字混入事件」と騒いでいた私の認識もそうだった。)しかし、振り返ってその歴史や内容を見ると、正直世界史、コンピューター技術史のようで、各国の文化の相反なども含め、社会学的興味が尽きない。

 

先ずUnicode(ユニコード。「統一文字符号規格」とでも訳すべきでしょうか?)といっても、色んなのがある。

Microsoft Windowsが内部的に使っていると思われる(WEBで見ると矢張り使っているようです)2バイト「可変長」のUTF-16(注1)、4バイト固定長のUTF-32(注2)、更にこれらが処理するチップによってビッグエンディアン、リトルエンディアン(注3)に派生し、WEBの世界で現在主流(注4)の1バイト可変長UTF-8がある。

注1:2バイト、65,536文字では収まらなくなって「サロゲートペア(Surrogate-『代用対』いうようですが、既に日本語じゃないよね)」を使った4バイト表現もあります。

注2:1バイトで表せる文字を4バイ使うので不要な記憶容量は膨大ですが、DBの世界では使われているそうな。

注3:「上位下位」が正順なのがビッグエンディアン。「下位上位」と逆順なのがリトルエンディアンで、32ビットでは「下位2バイト(下位上位)上位2バイト(下位上位)」となります。

注4:既にMicrosoftやPCがデファクトスタンダードではない、という象徴ですね。

 

誤解を恐れずにまとめると、Unicodeで統一されたのは統一文字符号表(あの「U+<16進数>」という奴)だけで、その表に載る文字符号を正規化する方法(注5)や、その現実のコンピューター内表現(注6)は色々ある、ということのようだ。

注5:UTF(Unicode Transformation Format)とはUnicode文字のエンコード形式、ユニコード表に載った文字符号の「正規化」と理解しています。この「正規化(normalization-データ等を一定の規則に基づいて変形し、利用しやすくすること)」というのも解り難い言葉ですが、「理解できる方程式がある」ことだと思います...人間は理解できないこと、ものを恐れるので、それに(時には無理やり)理屈をつけて理解しようとします-まぁこのような社会心理学的分析はまたの機会にしましょう。

注6:「文字符号化方式(もじふごうかほうしき、英: character encoding scheme、CES)とは、符号化文字集合で文字に対応付けた非負整数値を、実際にコンピュータが利用できるデータ列(通常、バイト列)に変換する符号化方式。」(Wiki)ざっくり言えば↑で触れたビッグエンディアン、リトルエンディアンのことです。

 

しかし、この「統一の理想と努力の結果が分断と派生を促進する」という逆説は(私にとっては)極めて人間的で興味深い。

 

そもそも何でUnicodeなんて作ろうとしたのか?

大型の事務所キャビネ2つ分くらいの大きさの、デジタルエクィップメント社(DEC)最新のPDP-11という「16ビットミニコンピューター」がUnixで動かされ、ベル研究所が開発言語として生み出したCについて「プログラミング言語C」として纏めて、発行したのが(偶々私が社会人になった)1978年、(人類の歴史から考えればほんの一瞬である)たった44年前である。(注7)

注7:その「最先端コンピューター」の処理能力も、現在から見れば極めて低く、現在は比べる方法もないが「後期のPDP-11では、仮想記憶をサポートするメモリ管理ユニットが使われた。物理アドレスは18ビットまたは22ビットに拡張され、256KBまたは4MBのメモリを扱えるようになったが、1つの論理アドレス空間は16ビット(32Kワード)に制限されたままだった。」という記述で十分だろう。

その後のコンピューターと通信の発展はすさまじく、民生分野でも8ビット→16ビット→32ビット→64ビットのCPU、1995年あたりからのインターネット接続(ダイアルアップ→ADSL→光ファイバー)等情報の国際的交換が促進され、商業的にも同一情報機器製品の国際的販売が行われるにつれて、ネットワーク(ソフト)インフラとしてのUnicodeの登場は必須だったのだろう。事実UnicodeのVer 1.0は(これも偶々であるが、私が北米駐在となった)1991年に行われた。

 

しかし、その理想と努力は異なる利害や技術的発展に影響を受ける。

 

先ず西欧アルファベット社会はASCIIでカバーできたかもしれないが、中東、アジアの言語、文字の存在が正規化を邪魔する。(注8)更に各国間の国際的力関係(注9)やその文化的特徴も見逃せない。(注10)

注8:大体「'?'禁則文字混入事件」も、U+200E(Left-toーRight-Mark)の為だし、それは右から左に読む中東文字の存在があったために必要となったものです。

注9:国家間の国際標準化を巡る暗闘に加え、当時のソフトの巨人Microsoftは勿論、PC互換機等のハードメーカーの力等。しかし、当時UTF-16を内部表現として使ったMicrosoftの読み誤りが、現在のUTF-8主流の中でどうなるか微妙ですね。

注10:東アジア圏の規格化とりまとめに(当時は経済的な国際競争力が高かった)日本が指揮役をとろうとするも、「アジアのラテン系」である韓国が口出ししてゴールポストを動かすような話もあったそうです。(1996年のUnicode Ver 2.0における「ハングルの大移動」はこれをキーワードにググると、韓流が結構好きな私にはエンターテインメントの世界です。)

 

このような複雑な歴史を持つCaos的Unicode世界で、商売としてプログラミングしているプロの方は大変だ。現在も「WEB主流の可変長UTF-8 vs. 初期のUnicode、2バイト主流UTF-16」の戦いは続いているようだ。(末尾の「Unicodeと、C#での文字列の取扱」参照)しかし、ソフトやアプリの開発でいつもこんな検討を行わなくてはならない方々は寿命が縮むのではないだろうか?

 

結局、趣味プログラマーの私としては「Unicodeには触るな!」を家訓にしたことが正しかったと確信している。「Unicodeの世界と歴史」は読んで楽しむのが一番、ゆめゆめこれでメシを喰おうなんて考えないように。

 

【参考サイト】

Unicode - Wiki

UTF-16 - Wiki

UTF-32 - Wiki

UTF-8 - Wiki

Unicodeの歴史

Unicodeと、C#での文字列の扱い

 

【バグは続くよ何処までも】シリーズでひつこく原因追及し、仮設と推論を重ね、ワイド文字対応が不可避ということで、同プログラムで使っているBCCSkeltonクラスをワイド文字対応させました。今後(「ワイルドカードの'?'等の禁則文字の混入」という誤った表現(注)を改め)「ファイルパスに入っているUnicodeの一般句読点文字(General Punctuation Characters - U+2000~U+206F)」を除去するFileNameCleanerプログラムの解説を行う上で、こちらを先に紹介します。なおタイトルの【BCCSkelton-WCV】は【BCCSkelton-Wide-Characters-Version】の謂です。
注:元々Windows (OS) はUnicodeを使っており、それをASCIIベースの8bit文字列に変換する際に、コード体系上存在しないコードに対して'?'(0x3F)を割り振るのは仕様であり、バグではないと理解しています。

このCMNDLGWはASCII(SJIS)対応のCMNDLGクラスの内、文字を扱う「ファイルを開くダイアログ」と「フォールダー指定ダイアログ」のみをワイド文字対応させています。(カラーダイアログやフォントダイアログはいじっていません。)またワイド文字化に伴い、コンストラクターを廃止する等、コードを見直して少し手直しを入れています。しかし、これは比較的簡単なワイド文字化だったので最初のサンプルとしては適切だと思います。


基本的にワイド文字対応を行う場合、以下を行いますので、この点に注意してご覧ください。
(1)文字列は8バイトのchar型から16バイトWCHAR(wchar_t)型に替える。その為にwchar.hをインクルードする
(2)今まで使っていた8バイト用のWin32API関数や構造体は(ワイド文字用のものが必ずあるので)ワイド文字用のものを使う。探す際のヒントは、以下の通りです。
 ①(関数名)→(関数名)W や"str"等の文字列を意味する文字が入っている関数名であれば"strW"に置き換えて検索する。
 ②(構造体名)→(構造体名)Wや①と同様に文字列を彷彿させる名称にWを付加したものを検索する。
(3)文字列定数は"なんちゃら"→L"なんちゃら"とする。単一文字の場合も'〇'→L'〇'とする。
(4)文字列のヌル終端はNULL(8バイト)でなく、明示的に16バイトのL'\0'とする。

以下では(最初だから丁寧に)変更点やポイントを赤色表示してみます。

////////////////////////////////////////
// CMNDLGWクラスヘッダー(定義ファイル)
// 注:使用時は"wchar.h"を読み込むこと
////////////////////////////////////////

class CMNDLGW
{
public:    //メンバー変数
    WCHAR        m_FileName[MAX_PATH] = {0};    //ファイル名の格納場所
       WCHAR        m_Path[MAX_PATH] = {0};        //ファイルパスの格納場所
    COLORREF    m_Color = 0;                //色の格納場所
    HFONT        m_Font = 0;                    //フォントハンドルの格納場所
//(解説:C++11からクラスでも宣言時初期化が可能になりました。この為、初期化だけのコンストラクターを廃止しました。)

public:    //メンバー関数
    ~CMNDLGW();                            //デストラクター
    WCHAR* GetFileName(HWND, WCHAR*, bool, WCHAR*, WCHAR*, WCHAR*, DWORD);
    COLORREF GetColor(HWND);
    HFONT GetFont(HWND);
    WCHAR* GetPath(HWND, WCHAR*);
};

//デストラクター
CMNDLGW::~CMNDLGW() {
    if(m_Font)
        DeleteObject(m_Font);
}

WCHAR* CMNDLGW::GetFileName(HWND hWnd, WCHAR* Filter, bool Open = TRUE,
                WCHAR* Ext = NULL, WCHAR* IniDir = NULL, WCHAR* Title = NULL,
                DWORD Flag = OFN_EXPLORER | OFN_FILEMUSTEXIST | 
                OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT) {

    if(IniDir)    *m_FileName = NULL;    //m_FileNameが初期化されている必要がある
    bool success = FALSE;
    OPENFILENAMEW ofn;                //オープンファイルダイアログ構造体
    ZeroMemory(&ofn, sizeof(ofn));    //ゼロ初期化
    ofn.lStructSize        = sizeof(ofn);
    ofn.hwndOwner        = hWnd;
    ofn.lpstrFilter        = Filter;
    ofn.nFilterIndex    = 1;
    ofn.lpstrFile        = m_FileName;
    ofn.nMaxFile        = MAX_PATH;
    ofn.lpstrTitle        = Title;
    ofn.lpstrInitialDir = IniDir;
    ofn.Flags            = Flag;
    ofn.lpstrDefExt        = Ext;
    if(Open)
        success = GetOpenFileNameW(&ofn);
    else
        success = GetSaveFileNameW(&ofn);
    if(success)        return m_FileName;
    else            return L'\0';
}

COLORREF CMNDLGW::GetColor(HWND hWnd) {

    static    CHOOSECOLOR clr;
    static    COLORREF cref[16];

    clr.lStructSize        = sizeof(CHOOSECOLOR);
    clr.hwndOwner        = hWnd;
    clr.hInstance        = 0;
    clr.rgbResult        = m_Color; //CC_RGBINITフラッグがあるとこれがデフォルトになる
    clr.lpCustColors    = cref;
    clr.Flags            = CC_RGBINIT;
    clr.lCustData        = 0L;
    clr.lpfnHook        = NULL;
    clr.lpTemplateName    = NULL;
    if(ChooseColor(&clr))
        return (m_Color = clr.rgbResult);
     else
         return NULL;
}

HFONT CMNDLGW::GetFont(HWND hWnd) {

    if(m_Font)
        DeleteObject(m_Font);
    //フォント情報をChooseFontダイアログ構造体から得る
    static    CHOOSEFONT    cf;
    static    LOGFONT        lf;
    cf.lStructSize        = sizeof(CHOOSEFONT);
    cf.hwndOwner        = hWnd;
    cf.hDC                = 0;
    cf.lpLogFont        = &lf; //選択の結果はここに入る
    cf.iPointSize        = 12;
    cf.Flags            = CF_EFFECTS | CF_SCREENFONTS;
    cf.rgbColors        = 0; //フォント色-ダイアログでも使われる
    cf.lCustData        = 0L;
    cf.lpfnHook            = NULL;
    cf.lpTemplateName    = NULL;
    cf.hInstance        = 0;
    cf.lpszStyle        = NULL;
    cf.nFontType        = SCREEN_FONTTYPE | REGULAR_FONTTYPE | BOLD_FONTTYPE | ITALIC_FONTTYPE;
    cf.nSizeMin            = 0;
    cf.nSizeMax            = 0;
    if(!ChooseFont(&cf))     //CHOOSEFONT構造体へのポインター
        return NULL;
    return (m_Font = CreateFontIndirect(cf.lpLogFont));
}

WCHAR* CMNDLGW::GetPath(HWND hWnd, WCHAR* Title = L"フォールダーの選択") {

    BROWSEINFOW BrowsingInfo;
    PIDLIST_ABSOLUTE ItemID;    //(解説:現在のMicrosoft Docに出ている型名に合わせました。)
    ZeroMemory(&BrowsingInfo, sizeof(BROWSEINFOW));
    BrowsingInfo.hwndOwner        = hWnd;
    BrowsingInfo.pszDisplayName    = m_Path;
    BrowsingInfo.lpszTitle        = Title;
    BrowsingInfo.ulFlags        = BIF_USENEWUI;    //CoInitialize関数によるCOM初期化必要
    ItemID = SHBrowseForFolderW(&BrowsingInfo);
    if(SHGetPathFromIDListW(ItemID, m_Path)) {
        GlobalFreePtr(ItemID);
        return m_Path;
    }
    else
        return L'\0';
}

 

「何だ、簡単じゃないか?」と思われたのではないでしょうか?実際ASCII8bit→Unicode(注)16bitワイド文字への移行は文字列やファイル操作が無い(少ない)場合は余り問題はありません。次回は更に難しくかったCSTRクラス→CSTRWクラスを取り上げます。

 

ps. なお、前にこんなことを書いていましたが、今回の騒動等、止むに止まれぬ際にはワイド文字対応を行うこと、ご容赦ください。またその際には【BCCSkelton-WCV】として少しづつ"W"クラスをためてゆきます。

【Unicode対応】ブランク20年間に生じた重荷

 

さて、前回Unicodeの「U+200E」(幅のない<ゼロ幅スペース>「左から右表示のマーク(LRM-LeftーTo-RightーMark)」)の存在を知り、おそらくASCII 0x3F('?')表示の原因はこれだ、ということでダンプを取り、(UTF-8のLRM)「0xE2、0x80、0x8E」の3バイトが入っていること、文字コード変換が必ずしも可逆性が無いこと、「この20年の間に世の中はFAT32のASCII(Shift-JIS)のMBSから、NTFSのUnicodeのWCSに移行していること」を痛感しました。つまり「MicrosoftのWindowsのバグ」と決めつけて始まったこのシリーズの終着駅は、「『FAT32のASCIIから、NTFSのUnicodeへの移行』を正しく理解していなかったお前(私)がバグ」であった訳です。

 

そんな訳で、昨日は俄か仕立てでUnicode(注)、C++(wchar_t)とWindows(WCHAR)のワイド文字のお勉強をしました。

注:一口にUnicodeといっても、現在主流といわれるUTF-8、UTF-16、UTF-32他があり、それが更にチップの性格からBE(ビッグエンディアン-上位下位バイトが順にストアされる)とLE(リトルエンディアン-上位下位バイトが逆にストアされる)に分かれるという眩暈と吐き気がする世界です。

更にその俄か知識で、FileNameCleanerで使うBCCSkeltonのCMNDLGクラスとCSTRクラスをワイド文字化し、それぞれ「ワイド文字対応CMNDLGWクラス、CSTRWクラス」を作成、その際に↑で触れたUnicodeの迷路世界の為のBOM(注)にぶち当たり、何とか(今のところUTF-16決め打ちですが)クリアして機能テスト迄こぎつけました。

注:様々なUnicodeの種類を冒頭のコードで判別する為のByte-Order-Markというものらしく、2バイトが基本のUTF-16だと0xFE、0xFF(BE)、0xFF、0xFE(LE)の2バイトのところ、UTF-8のBOMは0xEF、0xBB、0xBFの3バイトになる(同様にUTF-8のLRMお3バイトでしたが、UTF-16だと0x20、0x0E(または0x0E、0x20)の2バイトです)という面倒くささです。

 

【テストプログラム抜粋】

//Test for CMNDLGW
WCHAR* fp = g_Cmndlg.GetPath(m_hWnd, L"フォールダーの選択");
MessageBoxW(m_hWnd, fp, L"選択されたフォールダー", MB_OK | MB_ICONINFORMATION);
//(解説:MessageBoxのワイド文字版ですね。また文字列定数はLを付けてワイド文字にします。)

//(解説:今までMBSでは'?'が出ていたフォールダーパスがWCSでは簡単に表示されます。)

fp = g_Cmndlg.GetFileName(m_hWnd, L"すべてのファイル(*.*)\0*.*\0\0", TRUE);
MessageBoxW(m_hWnd, fp, L"選択されたファイル", MB_OK | MB_ICONINFORMATION);

//(解説:同じくファイルパスもWCSのまま('?'なく)表示されます。)

WCHAR buff1[MAX_PATH], buff2[MAX_PATH * 5];
lstrcpyW(buff1, fp);
for(int i = 0; buff1[i] != L'\0'; i++) {

//(解説:ワイド文字のNULL端末です。)

    wsprintfW(buff2 + i * 5, L"%4X ", uff1[i]);
}
MessageBoxW(m_hWnd, buff2, L"選択されたファイル(HEX)", MB_OK | MB_ICONINFORMATION);

/*(解説:とはいってもフルパスファイル名のWCHAR配列には例のLRMが存在していることを確認します。例:"2022"(32 30 32 32)の前後に2バイトの(U+)200Eが入っていることが確認できます。余談ですが、2バイト文字でもアルファベットはASCIIと変わらないのですね。また感じで最初に出てくる5E74がUnicodeの'年'です。なので、WCHARのデフォルトはUTF-16のようですね。

区 点 JIS SJIS EUC UTF-8 UTF-16

39 15 472F  944E  C7AF  E5B9B4  5E74     

)*/

 

//Test for CSTRW
CSTRW str(L"私の名前は〇〇〇〇です。\r\n");    //ワイド文字列による初期化
MessageBoxW(m_hWnd, str.ToChar(), L"メッセージ", MB_OK | MB_ICONINFORMATION);
CSTRW num = 67;                                //整数による初期化
MessageBoxW(m_hWnd, num.ToChar(), L"メッセージ", MB_OK | MB_ICONINFORMATION);

//(解説:CSTRWのコンストラクター試験です。)

str = str + L"私の年齢は" + num + L"歳です。";    //文字列の連結
MessageBoxW(m_hWnd, str.ToChar(), L"メッセージ", MB_OK | MB_ICONINFORMATION);

//(解説:CSTRWの演算子試験です。(歳がバレますね。))

str.ToFile(L"テキストファイル\0*.txt\0\0", L"txt", m_hWnd);    //文字列の書き込み

//(解説:ファイル書き込み試験です。)

num.FromFile(L"テキストファイル\0*.txt\0\0", L".", m_hWnd);    //文字列の読み込み

//(解説:ファイル読み込み試験です。)

num.Print();                //文字列の表示

//(解説:CSTRWの文字列表示ダイアログ試験です。)

while(num.GetLine(str)) {    //文字列の行切り取り
    str.Print();

//(解説:文字列切り取り試験です。)

num.FromFile(L"テキストファイル\0*.txt\0\0", L"C:\Dummy", m_hWnd);    //文字列の読み込み
while(num.Next(str)) {        //文字列の語句切り取り
    str.Print();
}

//(解説:語句切り取り試験です。)

 

一応上記すべての試験を満足のゆく形でパスしました。

ということで【バグは続くよ何処までも】シリーズ「'?'混入事件(Unicode編)」はこれでお終いのようです。今後もまたまたバグ騒動があると思いますので、その際はまたこのシリーズで取り上げましょう。

 

なお、CMNDLGWクラスとCSTRWクラスは(テーマ)プログラミングのFileNameCleaner(FAT32ユーザーの方へ!)の解説で紹介しましょう。また、今後のBCCForm and BCCSkeltonにも参考として同梱します。

 

前回↓からの続きです。

 

 

自分で作ったエディターが不出来でサクラエディターを使っているのですが、PowerShellのバッチファイルを書いていてこんな表示が出ました。この"文字U+200E"が今回の問題に関連しているんじゃないかとひらめきます。

で、「ユニコード(UFT-8)状態のサクラエディターに、プロパティダイアログの作成日時から日付文字列をコピーし、それのダンプを取ってみる」実験を行いました。

すると(ダンプ行3行目<0020の行>にある改行コード0x0D、0x0Aの後の"3F 32 30 32 :32 3F"、即ち"?2022?"の)ASCIIの'?'(0x3F)に該当する位置にあるのは(例:"2022"である0x32、0x30、0x32、0x32の前後)「0xE2、0x80、0x8E」の3バイト。これが'?'に対応する形で2022、4、11を挟んでいます。

「なんじゃ、こりゃ?」ということで、こいつを調べると、どうも「ゼロスペース」などという異名もある、ユニコード(Unicode)の「左から右表示のマーク(LRM-LeftーTo-RightーMark)」らしい。これは「インラインでテキストの方向を混在させるために、Unicode の right-to-left mark (RLM) 又は left-to-right mark (LRM) を使用する」ものだそうで、

Format characters(書式制御文字)

Unicode 表示 名称 備考
200B ZERO WIDTH SPACE  
200C ZERO WIDTH NON-JOINER  
200D ZERO WIDTH JOINER  
200E LEFT-TO-RIGHT MARK  
200F RIGHT-TO-LEFT MARK  

と呼ばれている由。(この制御コードで色々とWEB系でも問題があるらしく、これとかこれなどが目につきました。)

 

なら、UnicodeからASCII(SJIS)再度Unicodeと変化させるとどうなるかという実験も行いました。

すると元々の「(LRM-不可視です)2022年4月11日(LRM)」(UTF-8表示)はSJISに変換すると文字化けし、その際にLRMが'?'に変化し、再度UTF-8に戻しても(LRM)は消えたままで'?'(?2022?年?4?月?11?日)が残る不可逆的変化が観察されました。

 

まぁ、ファイル名で使用される文字セットはMS-DOS、16bitWindowsのFAT32時代で使われたASCII(MBS)から32bitWindowsの時代になってUnicode(NTFS)に変わり、Win32APIもA(ASCII)とW(ワイド文字)の二揃えからワイド文字だけに移行しているのだから当たり前かも。...と、ここまで書いてハッとしました。今頃になってWindowsはもうWYSWYG(What you see is what you get)ではなくなっちゃったんだ、「Win32 APIで取得したファイル名がASCII文字ならそれはもう本当のファイル名ではない」ということなんだ、と分かりました。確かにプロパティダイアログで作成日付をコピーしても'?'の文字は表示されません。(ワイド文字を使用しているから。)また、それをExplorerで「名前を変更」で出てくるエディットボックスに張り付けても'?'は表示されません。(矢張りワイド文字を使用しているから。)しかし、数字の前後に「1文字」あることはカーソルを動かせばわかります。(そしてそれが「UnicodeのLRMという3バイトの『ゼロスペース文字』」であることも、OSがUnicodeを使っている証です。)

今までワイルドカードとして使われた禁則文字'?'が混入した、と書いてきましたが、それはUnicodeでないASCII(SJIS)表示のエディターに張り付けたから(Unicode→SJIS変換)だということを思い知らされました。

 

しかし、Win32API関数を使う限り、ファイル名はASCIIで取得し、ファイル(ディレクトリー)の名前変更どころか、複写、移動、削除もASCIIのファイル名を使うしかないのです。そして↑で見たように「コードセット間の自動変換といえども不可逆変化がある」のであれば、そのような'?'が生まれてしまったファイルは(オリジナルのUnicode文字列を手作業処理する以外、Win32APIでは)処理ができないことになります。

 

旧いWin32APIでなく、現在のOSで使っている関数はないのか、とおもって調べているとシェル(SHObj.h)の"SHFileOperation"という関数を見つけました。(しかし、これの為に使うSHFILEOPSTRUCT(注)という構造体メンバーのファイル名もやはり"char*"で不安がよぎります。)

注:この構造体のwFuncメンバーに実行したい機能を指定すると、コピー(FO_COPY)、削除(FO_DELETE)、移動(FO_MOVE)、名前の変更(FO_RENAME)がすべてできるという優れものです。

 

実験してみましたが、矢張り

ということでWin32APIで取得したファイル名に '?' がある限り、ワイルドカードとして認識され(「1つのファイルに対して行ってください」)、エラーになってしまいました。

 

大分分かってきましたが、Unicodeのファイル名をASCIIに変換すれば、結局不可逆的に'?'が残ってしまい、ファイル操作ができません。もう少し調べて、考えてみることが必要ですね。

 

追記:このブログを書いてから、「きっとワイド文字対応の関数があるはずだ」ということで調べた結果、

ファイル操作→SHFILEOPSTRUCTW構造体+SHFileOperationW関数
フォールダー指定→BROWSEINFOW構造体+SHBrowseForFolderW関数+SHGetPathFromIDListW関数
ファイル指定→OPENFILENAMEW構造体+GetOpenFileNameW関数

があることを確認。これでもう一度FileNameCleanerを組みなおしてみようと思います。

 

前回(注)次の様に書きました。

「では、何故Cmd.exeやPowerShell.exeは受け付けるのでしょうか?

(ひょっとしてこの謎を解いたら、また書きます。)」

注:【バグは続くよ、何処までも】一応のピリオッドの後...

 

しかし、まだ謎は解けていません。

 

先ず旧MS-DOSをシミュレートしたCMD.exeでのテストは終わり、まず何をしてもMS-DOSの"ren (rename)"コマンドでは、プロパティダイアログの作成日からコピーした"?YYYY?年?MM?月?DD?日"というフォーマットの禁則文字を含むディレクトリーやファイルはファイル名を変更できません。(「コマンドの構文が間違っています。」)

 

次にテストしたのはShellExecuteEx関数を使ってPowerShell.exeにコマンド文を渡すことです。というのは、PowerShellだと時々コマンドラインからrenが成功するからです。

 

(最初にdirで「 2022 年 4 月 11 日のフォールダー」ディレクトリーを表示させ、renコマンドでファイル名変更をすると確かに変わっています。"Name"のところに注目してください。)

 

PowerShellを引数付きで起動し、同様の処理を行おうとすると、PowerShellに、

「Rename-Item -Path "C:\Dummy\?2022?年?4?月?11?日のフォールダー" -NewName "2022年4月11日のフォールダー"」

というコマンドを与えればよいです。またMS-DOS互換の

「ren "C:\Dummy\?2022?年?4?月?11?日のフォールダー" "2022年4月11日のフォールダー"」

でも可能です。これらを起動時の引数にするには「-Command {}」を加えてやればよいということです。即ち、

「PowerShell(.exe) -Command {Rename-Item -Path "C:\Dummy\?2022?年?4?月?11?日のフォールダー" -NewName "2022年4月11日のフォールダー"}」

とか、CMD.exeから呼び出すときは文字列にして、

「PowerShell(.exe) -Command "& {Rename-Item -Path "C:\Dummy\?2022?年?4?月?11?日のフォールダー" -NewName "2022年4月11日のフォールダー"}"」や

「PowerShell(.exe) -Command "& {ren "C:\Dummy\?2022?年?4?月?11?日のフォールダー" "2022年4月11日のフォールダー"}"」

で行けそうです。

それで実験すると、

"ren"、"Rename-Item"共に引数のパスが""で囲まれるとエラー。しかし、↑に示したように"ren"で引用符を外すと一応成功します。

 

ということで、まだまだこの謎は解けていません。ひつこく、ひつこく、調べてゆこうと思っています。

 

このブログでもゲームを取り上げたり、開発したりしましたが、内容は数学や経営学的な意味でのゲームという感じでした。

では純粋に楽しむ、アミューズメントゲームが嫌いか、というとそうではなく、はまりやすいゲーマー的な人間でもあります。

 

子供の頃はコンピューターゲームなどはなかったので、ボードゲームが結構好きでしたね。モノポリーなんて大晦日に兄弟で徹夜してやってました。また高校3年の受験勉強中に覚えた麻雀なんて最高のゲームですね。(おかげで大学は現役合格するも一留しましたが、何か?)

 

このブログにふさわしい話題ということで、私のコンピューターゲーム経歴なんぞ若干垂れてみたいと思います。

 

始めてコンピューターゲームに触れたのは、矢張り大学に入ったあたり(1970年代初頭)の喫茶店でしょうか?テーブルの代わりにゲーム台(注)があり、コーヒーなどを飲みながら1回\50~100のコインを入れていましたね。

注:白黒20万、カラー50万というのが当時のゲーム台の価格でした。

 

一番単純なのがテニスゲーム、道から外れないだけのドライブゲーム、どんどん長さが伸びてゆく蛇ゲーム(注)、勿論インベーダー(注)も大流行りでしたね。

注:今風の男の子のYuTuberのサイトにこんなのがありました。ヘビが伸びるゲームで1位を取ってみたい - YouTubeでも、当時は自分自身をまたぐことは許されておらず、自分にぶつかるとゲームオーバーでしたね。インベーダーゲームは今風の物は見当たらず、昔のままでした。

 

さて、大学時代にビリヤードで遊び、ビリヤードはボーリング場に会ったことから、ボーリング場のアーケードゲームもやりました。私が一番好きだったのは「月面着陸ゲーム」(注)一回\2~30と安かったのですが、熱くなってコインがなくなりました。

注:リンク先はwebで探していたら見つかったもの。(HTML5レトロ風ゲーム館)HTML5を使いこなしていますが、これだけレトロゲームを知っているのだから、作者は私と大して年齢は変わらないのではないでしょうか?

 

月面着陸ゲームは夢があり、熱くさせるので「もう一回!」と「はまるゲーム」になっていると感じるのですがどうでしょう?一方プログラミング的にみると単純のようで物理学の公式をうまく使ったゲームです。また失敗した時の爆発して部品(線)が放射される有様がとても印象的で、「いつかあんなゲームを作ってみたい」と思わせるものでした。