2026年7月14日火曜日

WPF/C#で jsonを使ったAppSettingsの実装

1.パッケージ追加

microsoft.extensions.configuration.json

JSON形式の設定ファイル(appsettings.jsonなど)を読み込むための構成プロバイダー

パッケージのインストール

Visual Studioツール>NuGetマネージャー>パッケージ マネージャー コンソール以下を実行
PM> Install-Package Microsoft.Extensions.Configuration.Json

microsoft.extensions.hosting

依存関係の注入 (DI)構成管理 (Configuration)などを統合的に管理するためのライブラリ(汎用ホスト)

パッケージのインストール
Visual Studioのツール>NuGetマネージャー>ソリューションのNuGet パッケージの管理
Microsoft.Extensions.Hostingを検索>選択>プロジェクトにチェック>インストール

2.appsettiongs.json追加
 (ファイル名は他と干渉しなければ任意)

・ソリューションエクスプローラー>プロジェクト選択>追加>JavaScript JSON 構成ファイル>名前入力>追加

・追加されたappsettiongs.jsonを右クリック>プロパティー>ビルドアクション>ドロップダウンで[コンテンツ]に変更

・出力ディレクトリにコピー>ドロップダウンで[新しい場合はコピーする]に変更

・JSONの例{

  "AppName": "MyApp",
  "MainWin": {
    "Title": "MainWindow", "Width": 800, "Height": 500, "Left": -877, "Top": 202 }

3.Jsonに対応するクラス追加

appsettingsをいくつかに分割する前提で、全体を管理するクラスを作る。名前は任意。
分割した個々の要素(Section)に対応するクラスを作り、そのインスタンスをプロパティーとして設定する。

    public class AppSettings
    {
public string AppName { get; set; } = string.Empty;
public MainWinSettings MainWin { get; set; } = new(); }

appsettings.jsonのセクション名と一致する名前でプロパティーを作る。
上記の例ではappsettings.jsonの中に”MainWin
"というセクションがある。

4.JSONとクラスの依存関係設定

JSONとクラスの依存関係設定

・プロジェクトダブルクリック>csprojに次のようなtemGroup追加

 	<ItemGroup>
<Compile Update="AppSettings.cs">
<DependentUpon>appsettings.json</DependentUpon>
</Compile>
</ItemGroup>

この設定を行うことで、ビルド時に実行ファイル(.exe)と同じフォルダにJSONファイルが出力され、DIコンテナ(Microsoft.Extensions.Configuration など)から正しく読み込めるようになる。

App.xaml.csのようにAppコンストラクタでServiceを構成することで、入出力先を変更できる。

5.App.xaml変更

StartupUriの設定を削除。

    StartupUri="MainWindow.xaml">

6.App.xaml.cs変更

コンストラクタでDIから受け取るServiceを登録

        public static IHost? AppHost { get; private set; }
        /// <summary>
        /// ホスト(Host)を構築し、設定ファイルとウインドウをDIコンテナに登録します。
        /// </summary>
        public App()
        {
            AppHost = Host.CreateDefaultBuilder()
                .ConfigureAppConfiguration((context, config) =>
                {
                    // 1. 環境(Debug/Release)に応じて読み込み元を決定
                    string jsonFileName = AppSettings.FileName; // "appsettings.json";
                    string targetPath = StorageService.GetSettingsFilePath(jsonFileName, true);
                    config.AddJsonFile(targetPath, optional: true, reloadOnChange: true);
                })
                .ConfigureServices((context, services) =>
                {
                    services.AddSingleton<StorageService>();
                    services.Configure<AppSettings>(context.Configuration);

                    var rootSettings = context.Configuration.Get<AppSettings>() ?? new AppSettings();
                    services.PostConfigure<AppSettings>(settings => { settings.PostConfigure(); });
                    // MainWindow や他の設定クラスに渡す AppSettings は、Options パターン(IOptions)を経由して取得したものをシングルトン登録する
                    services.AddSingleton<AppSettings>(provider =>
                    {
                        // ここで .Value を要求した瞬間に、AppSettingsが読み込まれ、PostConfigureが実行される
                        var options = provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<AppSettings>>();
                        return options.Value;
                    });
                    // 画面をDIコンテナに登録
                    services.AddTransient<MainWindow>();
                })
                .Build();
        }
        protected override async void OnStartup(StartupEventArgs e)
        {
            // ホストの起動
            await AppHost.StartAsync();

            // DIコンテナからMainWindowを取得(自動的にAppSettingsが注入される)
            var mainWindow = AppHost.Services.GetRequiredService<MainWindow>();
            mainWindow.Show();

            base.OnStartup(e);
        }

        protected override async void OnExit(ExitEventArgs e)
        {
            // アプリ終了時にホストをクリーンアップ
if (AppHost != null)
{
await AppHost.StopAsync();
}
AppHost.Dispose(); base.OnExit(e); }

7.MainWindow.xaml.cs変更

コンストラクタでDIから登録したServiceを受け取る。
MainWindowの設定に関わるものがある場合は
InitializeComponentの前に呼ぶ。

        public MainWindow(AppSettings settings)
        {
            this.DataContext = settings.MainWin;
InitializeComponent(); }


8.StorageServiceの実装例

        const string brandName = "MyComp";
        const string appName = "MyApp";

         /// <summary>
        /// AppData/Local/MyComp/MyApp 配下の指定されたファイル名のパスを返す。
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="copyIfNotExistsInAppData"></param>
        /// <returns></returns>
        public static string GetSettingsFilePath(string fileName, bool copyIfNotExistsInAppData)
        {
            #if DEBUG
                    // Debug時はexeと同じフォルダのファイルを返す
                    return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
            #else
            //Release(インストール後)はAppData配下の専用フォルダを返す
            string appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); // Local配下に作る場合はこちら
            //string appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); // Roaming配下に作る場合はこちら
            appDataFolder = Path.Combine((Path.Combine(appDataFolder, Path.Combine(brandName, appName))));
            // フォルダがなければ自動作成
            if (!Directory.Exists(appDataFolder))
            {
                Directory.CreateDirectory(appDataFolder);
            }
            string targetPath = Path.Combine(appDataFolder, fileName);
            // 初回起動時など、AppData にファイルがなければ exe 横からコピーする
            if (!File.Exists(targetPath) && copyIfNotExistsInAppData)
            {
                string sourcePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
                if (File.Exists(sourcePath))
                {
                    File.Copy(sourcePath, targetPath);
                }
            }
            return targetPath;
            #endif
        }

単純に保存すると日本語がのエスケープ付きバイナリ値(UTF16?)になる。次のようにするとUTF8で保存できる。

        public void SaveJson<T>(T data, string fileName){
            var options = new JsonSerializerOptions
            {
                Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
                WriteIndented = true
            };
            string filePath = GetSettingsFilePath(fileName, false);
            var json = JsonSerializer.Serialize(data, options);
            File.WriteAllText(filePath, json, Encoding.UTF8);
        }

WPF/C# マルチディスプレイで一方が未接続時にウインドウが表示範囲外に出てしまうときの対応

アプリ起動時にウインドウを前回終了時の位置に表示する場合、 マルチディスプレイの一方が未接続だとにウインドウがスクリーンの表示範囲外に出てしまうことがある。その場合、メインディスプレイに表示させるようにする。

1. スクリーンサイズを取得するために using System.Windows.Forms を有効にする。

プロジェクトをダブルクリック
csprojのXMLの<PropertyGroup>に次の行を追加

<UseWindowsForms>true</UseWindowsForms>

これだけだと全てのクラスに暗黙的に using System.Windows.Forms が適用され、MessageBoxのライブラリ間の競合が発生する。

    'MessageBox' は、'System.Windows.Forms.MessageBox' と  'System.Windows.MessageBox' 間のあいまいな参照です
これを避けるため、csproj次の設定を追加する
  <ItemGroup>
	<!-- WinFormsの自動usingから、衝突の原因になるSystem.Windows.Formsを削除 -->
	<Using Remove="System.Windows.Forms" />
  </ItemGroup>

スクリーンサイズを扱うクラスに using を追加
using WinForms = System.Windows.Forms;

2. ウィンドウがスクリーンに表示されているかの判断

//Windowの矩形領域
var windowRect = new Rectangle(Left, Top, Width, Height);
bool isVisible = false;
foreach (var screen in System.Windows.Forms.Screen.AllScreens)
{
// IntersectsWith を使い、現在のディスプレイの有効範囲(WorkingArea)と
// ウィンドウの矩形領域が重なっているか判定
if (screen.WorkingArea.IntersectsWith(windowRect))
{
isVisible = true;
break;
}
}
// どのディスプレイの範囲内にも入っていない、メインディスプレイの左上に配置、または画面右上あたりへ
if (!isVisible)
{
this.Left = 100;
this.Top = 100;
}

2026年7月12日日曜日

MSBuild プロジェクトビルドの出力の警告レベルを変更する手順

「発行」でエラーが発生し、原因を調べるためにビルド警告レベルを「詳細」に替えましたが、その後もとに戻そうとして設定方法を忘れて困ったので、メモしておきます。
ほとんどAIの回答のままです。

Visual Studio の「プロジェクトのプロパティ」ではなく、「全体のオプション」 画面にあります。

  • 設定画面の開き方と項目[ツール] メニュー を開きます。
  • [オプション] を選択します。
  • 左側のリストから [プロジェクトおよびソリューション] を展開します。
  • [ビルドおよび実行] を選択します。
  • 右側にある以下の 2つの項目 を変更します。
    「MSBuild プロジェクトビルドの出力の記述詳細度」
    「MSBuild プロジェクトビルドのログファイルの記述詳細度」
設定する値通常は「最小」や「標準」になっています。
エラーの詳しい原因を突き止めるために、これを 「詳細 (Detailed)」 または 「診断 (Diagnostic)」 に切り替えてください。
設定後に再度「発行」を試行すると、画面下の 「出力」ウィンドウ に、どのファイルや処理でエラーが起きたのかが非常に細かく分析・表示されるようになります。

WPFで単一exeのアプリを作成する方法

WPFアプリを単純にビルドするとすごい数のDLLが含まれる。

発行のオプションに「単一ファイルの作成」があるが、これだけだとまだいくつかDLLの残る。それらも全て含めてひとつのexeを作る手順。


ソリューションエクスプローラー > プロジェクト右クリック > 発行

> ターゲット/フォルダ

> 特定のターゲット/フォルダ > 場所 > 完了

>「公開準備が完了しました。」

> [すべての設定を表示]をクリック

【重要】 [配置モード]は[フレームワーク依存]にすること

 [ターゲットランタイム]を[移動可能]から特定環境指定に変更

> [ファイル公開オプション]が表示される

> 展開 > [単一ファイルの作成] をチェック

> 発行

次の警告が出る場合は、メッセージとおりプロジェクトのcsprojの<PropertyGroup>にメッセージにある設定を追加する。

2>'インターセプター' 機能は、この名前空間では有効になっていません。
プロジェクトに
'<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration</InterceptorsNamespaces>'

 を追加します。


これだけだといくつかdllが出力される。これもexeに含める場合は、ソリューションエクスプローラーでFolderProfile.pubxml に次の一行を追加   

Project>Properties>PublishProfiles>FolderProfile.pubxml 

<IncludeNativeLibrariesForSelfContained>true</IncludeNativeLibrariesForSelfContained>

(cs.projのPropertyGroupに追加してもよい。)


2025年4月9日水曜日

X68030とWindows間のMOによるデータ受け渡し

X68030とWindows間のMOによるデータ受け渡しを行ったが、いくつか躓く点があったのでメモしておきます。

結論としては、次のような手順で行えばデータ受け渡しが行えました。
・MOをスーパーフロッピー(IBMフォーマット)でフォーマットする。
・対象ファイル/ディレクトリ名を英大文字にしてコピーする。
 COPYALLで /U オプションを使うと大文字に統一してコピーできる
 ファイル名8文字以内、拡張子3文字以内するのが無難。
・ディレクトリをまとめてコピーしたい場合や元のファイル名を保持したい場合はLHZにアーカイブして受け渡しするとよい。

 例:LHA a -xr2z2 C:\X68DATA.LHZ A:\*.*

以下補足です。「ファイル」は「ファイルおよびディレクトリ」とし、「ファイル名」は拡張子も含めたものとします。

X68000でも同じ要領で行えると思いますが、試していないのでX68030で記述します。

システム構成の違いで現象が異なるかもしれません。また試した限りの手順なので、他の方法もあるかもしませんん。

ハード構成

X68030、SCSI接続MOドライブ(I/O DATA)
Windows 10、Windows XP、IDE接続MOドライブ(FUJITSU MCM3064AP)

IDE⇒USB変換器でWindows 10、Windows 11に接続してもIDE接続と同様の結果だったが、よく試していない。

ファイル/ディレクトリ名

半角英小文字が含まれていると、双方向でエラーが発生する。

X68030⇒Windows
エクスプローラに名前が表示されるが、アクセス不可。その中にサブディレクトが含まれている場合は表示されない。

Windows⇒X68030
「無効なメディアを使用しました」エラーが発生する。
ただしメディアそのものが無効なわけではなく、ひとつでも問題になるファイル名が含まれていると、そのファイルにアクセスした時点でエラーが発生する。COPYまたはCOPYALLコマンドではエラーが発生するまではコピーできる。

ワイルドカードを使ったCOPYとCOPYALLではファイルにアクセスする順序が異なるためと思うが、エラーが発生する箇所が異なる。

エラーが発生して到達できないファイルでも、フルパスでCOPYすると成功する場合がある。

Windowsで書き込みをした結果「無効なメディアを使用しました」エラーが発生するようになったMOでも、COPYやCOPYALLコマンドで書き込むことができる。

半角数字は問題なさそうだが、エラーが発生した場合は数字を除いてみる。

"_"は要注意で、半角英数字だけでもエラーになる場合がある。他の文字との組み合わせに問題があるかもしれない。

日本語ファイル名は使えるようだが、避けておいたほうが無難だろう。

物理フォーマット

古いメディアのため物理フォーマットが必要になることが多い。

Human68KのFORMATで「使用不可」になる場合がある。
その場合でもIBMFORMAT.Xで物理フォーマットできる場合もある。

また、WindowsでLogitecディスクフォーマッタで物理フォーマットすることで復活できることが多い。
https://www.logitec.co.jp/down/soft/format_s/d_fmt.html

ただし、私の環境ではフォーマット終了時に「フォーマットパラメータ作成に失敗」エラーが発生する。エクスプローラでフォーマットすることで(問題はあるかもしれないが)使用可能になる。

論理フォーマット

スーパーフロッピー(IBMフォーマット)でフォーマットする。
スーパーフロッピーとFATは似て非なるもので、パーティションテーブルの有無が大きな違いのようだ。

X68030ではフリーソフトのFIM.X、IBMFORMAT.Xでフォーマットできる。

Windows 10のエクスプローラのフォーマットではFATしかなく、かつフォーマット後”System Volume Information”なるデータが書き込まれるため、X68030では「無効なメディアを使用しました」となる。余分なファイルを全て削除すればエラーを回避できるかもしれない。

Windows XPでFATフォーマットしたものは、X68030でもアクセスできる。フォーマット後に余分はデータは書き込まれない。また、Windows XPで初期化したものは512バイト/ セクターになる。

なお、Logitecディスクフォーマッタはフォーマットの種類を表示するが、FATでフォーマットしたあともスーパーフロッピーと表示されるので、信じてよいかわからない。

もしFIM.XもIBMFORMAT.Xもなく、Windows XPもない場合は次の手順を試してみる。

・Windows 10でMOをFATでフォーマット。
・FIM.XあるいはIBMFORMAT.XをMOにコピー。 
・X68030でCOPYコマンドでフルパスでコピー。
 例: COPY C:\FIM.X A:

2024年11月27日水曜日

Excel VBA 備忘録 テクニック編

・画面表示を更新せずプログラムを実行する方法
    Rem 画面更新停止
    Application.ScreenUpdating = False
    Rem 画面更新再開
    Application.ScreenUpdating = True

・Me = 実行中のモジュールを示す変数
    MSのドキュメント「Me キーワード」 より抜粋

Meキーワード (keyword) は、暗黙的に宣言された変数のように動作します。 クラス モジュール内のすべてのプロシージャで自動的に使用できます。

クラスに複数のインスタンスが含まれる可能性がある場合、Me を使用して、コードが実行されているクラスの特定のインスタンスを参照できます。 現在実行されているクラスのインスタンスに関する情報を別のモジュール内のプロシージャに渡す場合、Me を使用すると特に便利です。

    例:実行中のWorksheetの名前の取得
        MsgBox Me.Name

 

2024年8月9日金曜日

X68030の電源 SH4 のATX化、バックアップ充電池交換

長年眠っていた X68030 本体と関連グッズを手放すことにし、動作チェックを行っていましたが、電源が故障してしまいました。電源を取り外し、コンデンサなどの部品を順次取り換えてみましたがなかなか手ごわく、復活しません。

いろいろ調べてみるとX680x0の電源SH4のATX化がいくつか見つかり、試してみたところ、おかげさまで起動することができるようになりました。

まずはお礼を兼ねて、丁寧な図を載せてくださったmojiraさんのサイトをご紹介。
作成手順はこちらのサイトをご覧ください。

X68000電源換装
http://mojira68.blog24.fc2.com/blog-entry-1.html

類似の製品がbeep-shopなどで販売されてるようですが、品切れ状態が続いているので私も自作することにしました。

これは以前に満開製作所が作ったATX/SH4変換ケーブルがもとなっているようです。電源自体は対応する電圧どうしを繋ぐだけですが、ATXのPower Onのシグナルが負(GND)に対し、SH4は正(+)状態と反対なため、ロジックICで反転させています。これによりX68030 本体のスイッチで電源ON/OFFが行えます。

X68030 は電源スイッチをOFFにしたあと、フロッピーの状態のチェックなどを行ってから電源が切れます。この間LEDが点滅しますが、これもちゃんと行われます。

手っ取り早く代用品が必要な場合は、SH4とATXで対応する電圧の線を繋ぎ、ATXのPower OnをGNDにつなげば使えるかもしれませんが、乱暴な電源ON/OFFは本体にダメージを与えかねません。ロジックIC(7404)が入手できるなら先人の成功例に倣うのがよいでしょう。

最初、電源ON後にちょっとだけフロッピードライブの動作音がしたあとディスクを読みにいきませんでした。フロッピーディスクドライブのフラットケーブルはSCSIモジュール経由で本体底面に差しますが、右タワー用の二極コネクタを穴を通して渡すときに抜けてしまっていました。動作確認のときはコネクタが全部繋がっているか、確認しましょう。

とりあえずは元の電源のケーブルを利用して作成しましたが、元の電源も修理したかったのでコネクタ類を調達し、作成し直しました。

以下、調達した部品のまとめです。
価格は2024年7月時点で、税抜、税込混在していますので、目安とお考え下さい。

ATX電源
200Wの小ぶりなSFX電源でも動作しました。ただし、私のX68030にはハードディスクがありません。

DELTA ERECTONICS, INC. DPS-200PB
メルカリで中古1,200円で購入。サーバから取り外した電源とのことで、そのためかいささか音が大きい。継続的に使用するにはファンの交換が必要でしょう。

IN WIN POWER MAN IP-P300DF1-0
所持していたので試しました。こちらは300Wで音も静かです。

mojiraさんの記事では20ピンのATX電源ですが、もちろん24ピンでもかまいません。私はフロッピードライブ用の3本を追加の4ピンの方とつなぎました。

ちなみにATX電源には同じ電圧のピンがいくつもありますが、回路別に電流を分散させるのが目的で、根っこは同じところにつながっているようです。

ATXコネクタ
SH4代用電源専用とするなら直接線どうしを接続してしまうこともできますが、そうすると移動するときに本体と外部電源を一緒に運ばなければいけません。やはりコネクタを付けた方がよいでしょう。

ATX電源側がオスなので、メスのコネクタが必要です。
mojiraさんと同じく、ATXオスメス変換ケーブルを購入しました。
Amazonで400円前後で購入できます。

6ピンコネクタ 3.96mmピッチ
同じ形状のものが通販で見つかりました。後述の山本無線でも扱っていました。

若松通商 VHR-6N 40円 (コンタクトピン付き)
共立エレショップ  VHR-6N 16円 (コンタクトピン別)

2ピンコネクタ 3.96mmピッチ
右タワー用の5V電源用。
秋葉原駅前ラジオセンター2Fの山本無線電材店で同じ形状のものが見つかりました。

モレックス 5195-02 50円
コンタクトピン モレックス 5194TL 15円/本

型番が分かるとMarubeni Online Shopで通販でも購入できるのがわかりましたが、その前に試しに似たものを購入しました。一応代用にはなります。ただし、+-を差し間違えないよう気を付けましょう。

若松通商 VHR-2N 26円 (コンタクトピン付き)
共立エレショップ  VHR-2N 11円 (コンタクトピン別売)

3ピンコネクタ 2.5mmピッチ 
コンタクトピンの仕様が異なりますが、サイズ、ピッチが同じものが山本無線電材店にありました。

AMP171822-3 50円 コンタクトピン別売 

試しに購入したものはちょっと小さくお薦めできませんが、とりあえず動作確認は行えました。これも+-を差し間違えないよう気を付けましょう。

共立エレショップ  EHR-3 11円 (コンタクトピン別売)

ロジックIC 7404
汎用の7404を購入しました。

若松通商 M53204P (7404) 64円

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

ATX電源で本体の動作が確認電できたところで、SH4電源の修理を再開したところ、どの部品が決め手になったのかわかりませんが、修理に成功しました。

なにぶん素人なもので部品全取り換えまでは行わず、電解コンデンサ、ツェナーダイオードと、問題ありそうな部分だけ取り換えています。

問題部分を探すのに、Kunihiko Ohnakaさんの
X68000シリーズ電源の部品面から見た抵抗配置図https://drive.google.com/file/d/1aXbfzF_zwfEswQ6yfBAbI7jxxhpNHxB6/view
が役立ちました。

「部品実装状態での実測値」を見ながら測定したところ、R37が絶縁状態になっていたのと、R4が怪しい値でした。おかしくなるには理由があるわけで、その周辺の部品を交換しています。どの部品が問題だったかまでは突き止めていませんが、ZD32、D33、D35などを交換しています。

SH4電源修理のためにコンデンサ、抵抗、ダイオード等のセットをメルカリで購入しましたが、それに含まれていなかったものを探しました。

[]内の名称は配線図の部品番号と部品名です。
なお、互換性は私個人の判断ですので、保証の限りではありません。

ツェナーダイオード
[ZD51 HZS15-1L] 
HZ15-1, HZ15-1TA 14.1~14.7 500mW 若松通商 53円

FET
[Q1 2SK643/2SK724/2SK735]
2SK735 若松通商 481円

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

ついでながら、ディスプレイケーブルのDsub15ピン2列を3列のRGBケーブルに変換することも試しました。通販で部品を取り寄せるとき、送料の方が部品代より高くついてしまうので、ついでに買うものはないかとディスプレイのコネクタも追加したからです。

RGBケーブルが一本余っていたので、これのコネクタ部分を切り離し、2列のコネクタに接続しました。

X68030はVキーを押しながら起動すると640x480モードになります。線を繋ぎ替えるだけで液晶ディスプレイへ表示することができました。

Dsub15ピン2列のアナログディスプレイケーブルと3列のRGBケーブルのピンアサインの対照は次のようなWebページに載っています。

VGAケーブルを作って見た。(2列←→3列 D-SUB15ピン)
http://rdstyle.cocolog-nifty.com/gm/2020/02/post-3324c0.html#google_vignette

VGA端子のピンアサインと形状
https://ameblo.jp/holycater/entry-12467338511.html

既製品のRGBケーブルは製品により配線の具合が異なるようです。私が使ったのものは6,7,8ピンのRGBのGRDがケース、シールド線と接続されており、線としては出ていません。RGB信号線が個別にアルミ箔でシールドされ、その中に細い線が一本はいっており、これがGNDと接続されています。そこで、この細い線を2列コネクタRGB GNDに接続しました。これらはみなGRDとどこかで接続されています。シールド線は2列コネクタのケースに接続しました。これで液晶ディスプレイに表示できました。

私の場合の配線の対応は次のようになりました。

3列             2列
1                1
1GRD          2
2                3
2GRD          4
3                4
3GRD          5
13               14
14               15
シールド        ケース

ただ、解像度が640x480なので横長ディスプレイは使い勝手はよくありません。ブラウン管ディスプレイが壊れたときの緊急対応用に保管しておきます。

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

一難去ってまた一難で、フロッピーディクドライブが不調になりました。

「システムを起動できませんでした。リセットして下さい。」
「エラーが発生しました。リセットして下さい。」

などが発生します。ドライブを取り外してグリスを塗ったりしていたので、何か悪さをしてしまったかと、再度チェック。さらに分解するとかえってとりかえしのつかない状態になりそうなので、いったん諦める。

いろいろ調べると、SRAMのバックアップ電池が切れているとこの症状がでるそうでそれを交換したら直ったという記事を見つけました。

SHARPパソコン X68000 EXPERT-HDのバックアップ電池交換http://kanchan707.web.fc2.com/X68KBATT.htm

X68030の底蓋を外してメインボードを取り出し、バッテリーをとりはずします。バッテリーの型はPanasonicのVL-1220/HFK。これとズバリ同じものはがみつからず、端子タイプが異なるVL-1220/VFKをAmazonで購入。ただいま到着を待っています。AliExpressにVL-1220/HFKがあるにはあったのですが、値段も安くなく到着がいつになるかわからなのでやめておきました。

ちなみにML-1220というのもありますが、VLがバナジウムリチウム二次電池に対し、MLはマンガンリチウム二次電池で、仕様を見ると出力電圧はどちらも3Vですが充電電圧が異なります。発火の原因になるといけないので、MLで代用するのはやめた方がよさそうに思います。

さて、バッテリーが届いたのでさっそく交換する。端子タイプが縦型だが、折り曲げるとうまい具合に穴にはいった。

交換後すぐは以前と同じ状態が発生。これで諦めるのはつらいので、再度ドライブを取り外してコネクタをしっかりはめ直してみる。おお、起動した!

バッテリーの交換が功を奏したのか、コネクタの接続が甘かったのか定かではないが、いずれにせよ古いバッテリーは電圧が0.3Vくらいで寿命が尽きていたので、交換が必要だったのは間違いない。



2024年6月2日日曜日

Windows 11 Excel VBA で InternetExplorerが使えなくなったことへの対応

Excel VBAでこれまで使えていた InternetExplorerがWindows 11で使えなくなりました。(と思ったら、また使えるようになっていた。よくわからないが、いずれは使えなくなる前提でコーディングしておいた方がよいでしょう。)

Excelはサポート切れの2010と、ちと古いです。

ちょっと調べると、代替え策としてフォームのWebBrowserを使う方法が出てきます。Navigateでurlのページを表示するだけならこれが良さそうです。ですが、DOMのメソッドを使って操作する場合、使えないメソッドがいくつかあります。たとえば

でエラーが発生します。他にも使えないメソッドがあるかもしれません。

これへの解決策として、次のコードのようにDocument.Allを使って全エレメントを取得し、個々にnameやclassNameをチェックする方法が出てきます。

    Dim elm As HtmlElement     For Each elm In WebBrowser1.Document.All I       if elm.GetAttribute("className") = "someName" Then             Debug.Printh elm.InnerText         End If     Next

これは単発で使うには十分かと思いますが、ループで使用するにはいささか重い処理になりそうで、検討の余地ありです。加えてclassには複数の値が設定されることが多く、"="での比較はほとんど場合に不適切でしょう。

なお、「このページのスクリプトでエラーが発生しました」が発生する場合は、navigateを呼ぶ前に次の行を追加します。

    WebBrowser1.Silent = True     

次に見つけたのはMSXML2.XMLHTTPを使う方法です。次のようなコードでXmlDocumentを作ります。

    Dim MyRequest As Object     Set MyRequest = CreateObject("MSXML2.XMLHTTP")     MyRequest.Open "GET", url     MyRequest.send     Do Until MyRequest.readyState = 4: DoEvents: Loop     Set xmldoc = MyRequest.responseXML

これで成功する場合は良さそうですが、これは TLS 1.2 以上には対応していないという問題があるようです。次のようにするとよいという指摘があります。

    Set MyRequest = CreateObject("MSXML2.ServerXMLHTTP")

私は MSXML2.XMLHTTP.6.0 を使いましたが、今のところ問題ありません。

    Set MyRequest = CreateObject("MSXML2.XMLHTTP.6.0")

なお、MSXML2.XMLHTTP.6.0 でエラーが出る場合は参照を追加します。

    ツール⇒参照設定⇒Microsoft XML v6.0追加

    その他、次のライブラリ参照も必要です。
    Microsoft HTML Object Library
 後述のサンプルにあるRegExpを使うには、このライブラリも必要です。
    Microsoft VBScript Regular Expressions 5.5

さらに調べた結果、次の方法が見つかりました。これはHTMLをDOMDocumentとして取得し、それからHTMLDocumentを作ります。

これはよい結果でしたので、次のようなFunctionを作り、実際に使用しています。

Public Function GetHTMLDoc(url As String)     Dim httpReq As Object     Set httpReq = CreateObject("MSXML2.XMLHTTP.6.0")     httpReq.Open "GET", url, False     httpReq.send (Null)     Do Until httpReq.readyState = 4: DoEvents: Loop     Dim htmlDoc As IHTMLDocument     Set htmlDoc = New HTMLDocument     htmlDoc.Write httpReq.responseText      ' 書き込みの完了を確定させる     htmlDoc.Close      ' ページの読み込みが完全に完了するまで待機する
    Do Until htmlDoc.readyState = "complete": DoEvents: Loop     Set GetHTMLDoc = htmlDoc End Function

この方法はWebページを表示する必要がなく、DOMメソッドで操作するのが目的な場合に適しています。表示を行わないため、処理は高速です。表示も必要な場合は、WebBrowerにHTMLをセットすればよいでしょう(試していませんが)。

さて、これで作った HTMLDocument ですが、ちょっと奇妙な動きをします。

htmlDoc.GetElementsByClassName(className)

は動作します。(先頭の"G”は大文字です。)

ですが、個々の HtlmElement には getElementsByClassName を適用できません。

この範囲で十分であれば、従来のIEと同様の方法で利用することができます。

ですが、ある Element の子要素から getElementsByClassName で要素抽出をしたい場合には処理が複雑になります。

そこで、次のようなFunctionを作りました。あるエレメント内の指定のtagNameのものからclassNameを含むものを抽出します。

Public Function GetElementsByClassName(htmlElm As IHTMLElement, tagName As String, className As String)     Dim elm As Object, attr As String, list As Collection     Dim re As New RegExp: re.pattern = "(^| )" & className & "( |$)"     Set list = New Collection     For Each elm In htmlElm.getElementsByTagName(tagName)         If re.test(elm.getAttribute("className")) Then             list.Add elm         End If     Next     Set GetElementsByClassName = list End Function

正規表現はclass属性の最初、または最後か、前後がスペースで区切られているものとマッチします。

    re.pattern = "(^| )" & className & "( |$)" 

戻り値の型はCollectionになります。インデックスを使う場合、配列と異なり先頭の要素のインデックスは 1 になることに注意してください。

加えて、次のようなFunctionも作りました。私の場合、getElementsByClassNameで見つかった最初の要素を使うことが多く、IEを使った処理では次のようなコードでした。

    name = someElm.getElementsByClassName("name")(0).innerText

そこで、Collectionで返さず最初に見つかった要素を返すようにしました。

Public Function GetFirstElementByClassName(htmlElm As IHTMLElement, tagName As String, className As String)     Dim elm As Object, val As String     Dim re As New RegExp: re.pattern = "(^| )" & className & "( |$)"     For Each elm In htmlElm.getElementsByTagName(tagName)         If re.test(elm.getAttribute("className")) Then             Set GetFirstElementByClassName = elm             Exit Function         End If     Next     Set GetFirstElementByClassName = Nothing End Function  

呼び出し方は次のようになります。

    name = GetFirstElementByClassName(someElm, "div",  "name").innerText

戻り値がNothingとなる場合があるなら、次のようなチェックを入れます。

    Set elm = GetFirstElementByClassName(someElm, "div",  "name")     If Not elm Is Nothing Then name = elm.innerText

HtmlDocumentの先頭からclassNameのものを探したい場合は htmlDoc.body または htmlDoc.DocumentElement を htmlElm として渡します。

    divs = GetElementsByClassName(htmlDoc.body, "div",  "someClass")

私の場合はclassNameでエレメントを抽出する場合はtagNameも決まっているのでこの方が都合がよく、かつ処理速度も多少なりとも速くなっているだろうと思います。

もし異なるtagNameのものも含めて抽出したい場合は、たとえばtagNameにvbNullStringを渡し、次のような使い分けをすればよいでしょう。

    Dim elms as IHTMLElementCollection
    If tagName = vbNullString Then
        Set elms = htmlElm.all
    Else
        Set elms = htmlElm.getElementsByTagName(tagName)
    End If

結果的にはWebBrowserによる表示が不要なため、この変更で処理速度は格段に速くなりました。

補足

この例で生成する HtmlDocument にはいくつか制約があります。

・sectionなど非対応のtagがあり、その場合はHTMLUnknownElementとなり、単独のElementとして作られるがDOM構造には含まれず、getElementsByTagName、allなどのメソッド/プロパティーが使用できない。

原因ははっきりしませんが、フリーズすることがありました。速度が速くなりすぎ、Excelの再描画追いつかないためかもしれません。そんなときは再描画の一時中止/再開を試してみてください。  

    Application.ScreenUpdating = False         再描画が発生する処理     Application.ScreenUpdating = True 

 

2024年5月26日日曜日

Mac インストールUSBドライブ作成エラー ...app does not appear to be a valid OS installer application.

Webページ https://support.apple.com/ja-jp/101578 を参照し、
Macbool Air 2011 A3169 用に High Sierra のインストールUSBドライブを作成しようとしたらエラーが発生。

Webページ「ターミナルを使って起動可能なインストーラを作成する」に記載されているHigh Sierra用のコマンドをそのままコピペ(MyVolumeは適宜変更)でターミナルで実行するとエラーが出る。

コマンド
sudo /Applications/Install\ macOS\ High\ Sierra.app/Contents/Resources/createinstallmedia --volume /Volumes/MyVolume

エラー
/Applicaiont/macOS\ High\ Sierra.app does not appear to be a valid OS installer application.

これは日本語環境でAppStoreからインストーラをダウンロードするとインストーラの名前が

Install macOS High Sierra.app

ではなく

Install macOS High Sierraインスール.app

となり、異なっているため。どうも"Install macOS High Sierra.app”という名前をチェックしているようなので、ファイル名を英語表記に合わせて変更しないとダメなようだ。

MyVolumeは実際のターゲットのマウントポイント名に変更するが、実際のマウントポイントが表示名と異なることがある。この場合は見た目は一致していてもマウントポイントが見つからないというエラーが出る。

ディスクユーティリティーでUSBドライブのプロパティーを見て、コマンドのMyVolumeをマウントポイントに一致させる。

あるいはディスクユーティリティーでUSBドライブを消去し、名前を”MyVolume”でフォーマットすればコマンドはそのままでよい。

MyVolumeは createinstallmedia 実行後に既定の名前に変更される。










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)




2024年2月25日日曜日

Excel VBA 備忘録 基礎知識編

ときどきExcelのVBAを書きますが、普段使わない言語だとなにかとつまづきます。
自分自身のための備忘録ですが、きっと初心者ならだれもつまづくところだと思います。

VariantとObject
    Variant
     すべての変数。
     Integerなどの値のみを持つプリミティブなデータ型とオブジェクト型のどちらも代入可能。
       配列もプリミティブなデータ型なのでVariantで宣言した変数にしか代入できない。    
    Object
     データとプロパティー、メソッドがカプセル化されたもの。
       オブジェクト型のみ代入可能。
       これにプリミティブなデータを代入すると「オブジェクトが必要です」エラーとなる。

Null, Nothing, Empty, vbNullString の違い
 Null             Variant型変数内に有効な値が入っていない状態
 Nothing       Objectとして空の状態
 Empty         Variant型として空の状態
   vbNullString 値が0のString 

代入時にSetが必要な場合、不要な場合
   Setが必要な場合
        Objectに代入する場合で、値だけでなくObjectのプロパティーの設定が行われる。
 Setが不要な場合
     Integerなどのデータ型に値のみ代入する場合。

Sub呼び出しでCallが必要な場合
 引数を()でくくる場合。

コンパイルエラー:プロパティーの使い方が不正です
    Setを付けずにObjectへ代入しようとした場合。

コンパイルエラー:型が一致しません
 Objectで宣言した変数ににIntegerなどの値をSetしようとした場合。
  Dim o as Object: Set o = 1

コンパイルエラー:オブジェクトが必要です
   Variantで宣言した変数に値をSetしようした場合。
  Dim v as Variant: Set v = 1
 Stringで宣言した変数にSetで文字列を代入しようとした場合。
  Dim s as String: Set s = "str"

コンパイルエラー:ユーザ定義型は定義されていません
    Dimや引数の型宣言で規定のデータ型にもユーザ定義型にもない名前が使われている場合。
    だいたいは書き間違え

実行時エラーオブジェクトが必要です
    値がNothingのオブジェクトのプロパティーにアクセスした場合。
    Objectで宣言した変数にプリミティブなデータを代入しようとした場合。
    Objectで宣言した変数にプリミティブなデータを代入しようとした場合。

実行時エラー:オブジェクト変数またはWithブロック変数が設定されていません。
    Objectで宣言した変数にSetをつけずにオブジェクト、または値を代入しようとした場合。
     Dim o as Object: o = "a" 
     Dim o as Object: o = 1 

実行時エラー 400










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)でもエラーが発生しなくなるでしょう。




2023年9月4日月曜日

VB Excelのシートを名前で探す方法 高速版

VBAでExcelのシートを名前で探す方法を調べると、たいていループで名前が一致するものを探す方法が紹介されています。

たとえばこんなコードです。

    Sub Sample1(name As String)
      For i = 1 to Worksheets.Count
            if Worksheets(i).name = name Then
                Debug.Print Worksheets.Index
                Exit For
            End If
        Next
    End Sub

一回だけ実行する場合は十分高速で問題ないでしょう。しかしループの中で使用すると繰り返し実行されるため、場合によってはかなり時間がかかります。 かつ結果"シート名"と一致するものがない場合はシート数Xループ回数実行され、無駄に時間を浪費します。

上記の例ではWorksheets(i)と引数にindexを使用しています。またiの範囲がCountの範囲内なので、Worksheet(i)がエラーを起こすことはありません。

では、Worksheetsは引数にシート名を使用することもできるので、次のようなコードを試してみましょう。

    Dim sheet As Worksheet
    Set sheet = Worksheets("シート名" )

"シート名"のWorksheetが存在する場合は問題ないのですが、存在しない場合は「インデックスが有効範囲にありません。」というエラーが発生します。Worksheet(i)でもiが有効範囲外であれば同じエラーが発生しますので、内部では同様のループ処理が行われていることが想像できます。

今度は、つぎのような方法を試してみます。

    Sub Sample2(name As String)
        On Error GoTo NotFound
        Debug.Print Worksheets(name).Index
    NotFound:
    End Sub

これでエラーが回避でき、コードもすっきりしているかと思います。面白いことに、計測してみると単純ループの55~60%程度の処理速度となります。75~80%のスピードアッ!!とも言えます。内部的にはループと同等のことを行っていると思いますが、コンパイル済みかどうかの差が大きいのでしょう。

ループによる実行回数が多い場合はバカにできない差になります。

このひとつ前のブログ『InternetExplorer.getElementById 「オブジェクトが必要です」エラー対策』で使用した「On Error Resume Next」も試してみました。

    Sub Sample3name As String)
        Dim sheet As Worksheet
        On Error Resume Next
        Set sheet = Worksheets(name)
        If Not sheet Is Nothing Then
            Debug.Print Worksheets(name).Index
        End If
    End Sub

これは処理速度的にはSample2と同等でした。この例の場合ではSample2の方がコード的にもすっきりしていますが、使用する場面では使い道があるかもしれせん。

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

おそらくほとんどの場合は上記の方法で十分高速になると思いますが、シート数が多く、何十回も検索をするような場合にはDictionaryを使うとさらに高速になるでしょう。シート数が少ないとかえって逆効果になり得ます。

Rem シート名がnameのWorksheetを返す。
Function GetSheetByName(name As Variant)    
    static SheetNameDict As Object

    ’SheetNameDict初期化。
    'SheetNameDict未設定の場合、全ワークシートのKey=name, Value = sheetをセット。
    If SheetNameDict Is Nothing Then
        Set SheetNameDict = CreateObject("Scripting.Dictionary")
         For Each sheet In Worksheets
            Call SheetNameDict.Add(sheet.name, sheet)
         Next sheet
    End If

    On Error Resume Next
    Set GetSheetByName = SheetNameDict(name)
End Function

使用例
Dim sheet as Worksheet
Set sheet = GetSheetByName("someSheetName")
if Not sheet is Nothing then
    ....
End If


Sheetが追加される場合は次のようなコードが必要でしょう。

    If SheetNameDict.Exists(name) Then
        Set GetSheetByName = SheetNameDict(name)
    Else
        On Error GoTo NotFound
        Dim sheet As Worksheet
        Set sheet = Sheets(name)
        Set GetSheetByName = sheet
        Call SheetNameDict.Add(name, sheet)
        Exit Function
NotFound:
        Set GetSheetByName = Nothing

なお、このサンプルの場合はSheetが削除されるとその後存在しないSheetを返すことになり、オートメーションエラーを起こします。必要であればWorkbook_SheetBeforeDeleteイベントでSheetNameDictから削除、あるいはOnErrorで対処します。