変奏現実

パソコンやMMORPGのことなどを思いつくまま・・・記載されている会社名・製品名・システム名などは、各社の商標、または登録商標です。

この画面は、簡易表示です

パソコン

[三国英雄の夜明け]ノビシロが無い

微妙

英雄も戦力もアップしてるけど、サーバ内順位はジワジワと下がっている。

主に幕府のレベル上げで底上げ、稀に定計で底上げ

つまり、

絶賛、伸び悩み

珍宝の強化はミスりっぱなしの金食い虫。

魂玉の品質7化の動きが鈍く特に神兵とかは動きが見えない。

副将のスキル上げは順調に上がるけど1000万銀銭も消し飛ぶ。

ps.2022/9/4

幕府、戦記上げ中。

幕府の文書購入で黄金が吹っ飛び、幕府出陣中の英雄評価を上げるのに銀両が吹っ飛ぶので、

黄金や銀両の消費が少ない百貨市場他くらいしか手が回らない。

ps.2022/9/9

戦計。

緑系は概ねレベル7

これだけレベル6

青系がなかなかレベル5にならない。

半分程度
もう少し。
もう少し。
と、
ずーっと
思ってる。

ピンク系(9種類)はレベル4にすらならない。

最近見かけない

グレー系(12種類)は本来の色すら判らない

何色なのかな?

竜金探索で食料2000万取得したので、自動訓練放置したら、一晩で1000万も食いつぶしていた。

兵隊もいっぱい増えたので、国戦の弔慰戦功を利用してシーズンイベントを進める。

やっと14/22
残り8任務が鬼レベルなのかもしれない

ps.2022.9.13

反間計がレベル7になり、

長かった

やっと、戦力が1900万台になった。

戦力19,109,898



[JavaScript] new ActiveXObject (progId)っぽく

IEのJavaScriptで

var cn = new ActiveXObject("ADODB.Connect");

みたいなことが出来たので、

Chromeでもできないのか考えてみた。

ActiveXObjectクラスのnew演算子でパラメータをplogIdとするOCXっぽいオブジェクトが戻って

くれれば嬉しいので、ストレートに

returnを使ってみた。

class ActiveXObject {
    constructor(className, json)
    {
        this.domain       = "xxx.xxx.xxx"; // この辺はC#側の設定を転記
        this.port         = "xxxx";         // この辺はC#側の設定を転記
        this.className    = className;
        this.objectID     = null;
        // 派生クラスの場合は再帰してしまうので、処理を省略
        if(typeof(className) !== 'undefined')
        {
            // classNameから当該クラスのJavaScript側クラスのオブジェクトを作成
            let cf = new Function('className', `return new ${className.replace('.', '_')}()`);
            let obj = undefined;
            try {
                obj = cf(className);
            } catch (ex) {
                throw new Error(`★new ActiveXObject("${className}")未定義クラス`);
            }
            // 初期設定値(json)があれば、設定する
            if (typeof (json) !== 'undefined') {
                obj['objectID'] = json['objectID'];
                obj.__setup__(json);
            }
            // 無ければ, C#のサーバに問い合わせる。
            else
            {
                // サーバー側にnewしたことを通知
                obj.init();
                // 放置してもサーバ側のオブジェクトは解放されないので、オブジェクトが不要になったら、適宜termメソッドを呼び出す事
                // this.term();
            }
            return obj; // new の戻り値
        }
    }
    // 設定する
    __setup__(json) {
        // プロパティにコピー
        for (let key of this.propertyNames) {
            let jj = json[key]
            if (typeof(jj) !== "undefined") {
                this[`_${key}`] = json[key];
            }
        }
        // プロパティ(コレクション)にコピー
        for (let key in this.propertyCollections) {
            let jj = json[key]
            if (typeof(jj) !== "undefined") {
                if (typeof (this[`_${key}`]) === "undefined" || this[`_${key}`] === null) {
                    this[`_${key}`] = {};
                }
                for (let idx in jj) {
                    let k = jj[idx].Name;
                    this[`_${key}`][`${k}`] = jj[idx];
                }
                // this[key]['Item'] = this[key]
            }
        }
    }
    // サーバー側にnewしたことを通知
    async init() {
        let json = await ActiveXObject.staticSendMessage('POST', this.domain, this.port, `ActiveXObject/${this.className}`);
        ActiveXObject.staticInit(this, json);
    }
    // オブジェクトの解放
    async term() {
        return await ActiveXObject.staticSendMessage('DELETE', this.domain, this.port, `ActiveXObject/${this.objectID}`);
    }
    // サーバー側にnewしたことを通知
    static staticInit(obj, json)
    {
        // 必須
        if (typeof (json["objectID"]) === 'Nothing')
        {
            alert("objectIDが取得できません。");
        }
        obj.objectID = json["objectID"];
        if (typeof (obj.onobjectID) !== 'undefined')
        {
            obj.onobjectID();
        }
        obj.SetProperties(json['activeXobject']);
        //
    }
    // 送信
    static staticSendMessage(method, domain, port, page, data)
    {
        let promise = new Promise( (resolve, reject) => {
            try {
                let parserFunction = this.parserFunction;
                let xhr = new XMLHttpRequest();
          // httpsにした方がいいのかもしれない
                let url = encodeURI(`http://${domain}:${port}/${page}`);
                xhr.open(method, url, true);
                xhr.responseType = 'text';
                xhr.send(this.staticEncodeHTMLForm(data));
                xhr.onload = function(e) {
                    if (xhr.readyState == 4) {
                        if (xhr.status == 200 ) {
                            try {
                                let json = xhr.response;
                                json = JSON.parse(json, parserFunction);
                                resolve(json);
                                return;
                            } catch (ex) {
                                console.log(`'${ex.toString()}'\n url:http://${domain}:${port}/${url}\n json:'${xhr.response}'`);
                            }
                        }
                    }
                };
            }
            catch (ex) {
                reject(ex);
                return;
            }
        });
        return promise;
    }
    // データをBODYの形式に変換
    static staticEncodeHTMLForm(data) {
        if (typeof(data) === 'undefined' || data === null)
        {
            return null;
        }
        let a = [];
        for (let aa of data) {
            a.push(aa);
        }
        return "params=" + JSON.stringify(a);
    }
    //
    static staticMakeUrlParameters(arg) {
        let rc = [];
        for (let idx = 1; idx <= arg.length; idx++) {
            rc.add(`p${idx}=JSON.stringify(${arg[idx - 1]})`);
        }
        return rc.join('&');
    }
    //
    static staticParserFunction(k,v)
    {
        try
        {
            if (typeof(v) === "string" && ((v.indexOf("function") === 0) || (v.indexOf("async") === 0)))
            {
                return eval(`(${v})`);
            }
            else
            {
                return v;
            }
        } catch (ex) {
            console.log(ex.toString());
        }
    }
}

newで使用する各ActiveXObjectのテンプレ・クラスは・・・

//  ADODB_Connection クラス
class ADODB_Connection extends ActiveXObject {
    constructor() {
        // とりあえず定番
        super();
        this.className = this.constructor.name.replace('_', '.');
        // プロパティ
        // 名前リストからプロパティを作成
        this.propertyNames = ['Attributes', 'CommandTimeout', 'ConnectionString', 'ConnectionTimeout', 'CursorLocation', 'DefaultDatabase', 'IsolationLevel', 'Mode', 'Provider', 'State', 'Version'];
        this.makeProperties(this.propertyNames);
        // プロパティ(コレクション)
        // 名前リストからダミーのメソッド(コレクション)を作成
        this.propertyCollections = {
            // https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/errors-collection-properties-methods-and-events
            'Errors': {
                'properties': ['Count', 'Item'],
                'methods': ['Clear', 'Refresh'],
                'events': []
            },
            // https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/properties-collection-properties-methods-and-events
            'Properties': {
                'properties': ['Count', 'Item'],
                'methods': ['Refresh'],
                'events': []
            }
        };
        this.makePropertiesCollection(this.propertyCollections);
        // メソッド
        // 名前リストからダミーのメソッドを作成
        this.methodNames = ['BeginTrans', 'CommitTrans', 'RollbackTrans', 'Cancel', 'Close', 'Execute', 'Open', 'OpenSchema'];
        this.makeMethods(this.methodNames);
        // イベント
        // 名前リストからダミーのメソッドを作成
        this.eventNames = ['BeginTransComplete', 'CommitTransComplete', 'RollbackTransComplete', 'ConnectComplete', 'Disconnect', 'ExecuteComplete', 'InfoMessage', 'WillConnect', 'WillExecute'];
        this.makeEvents(this.eventNames);
    }
}

“ADODB.Connection”というクラス名に使えなかいので、”ADODB_Connection”にしている。

makeXXXXは使いまわすのでActiveXObjectクラスで実装

    //
    /**
     * 名前リストからプロパティを作成
     * 
     * @param {String[]} propertyNames   名前リスト
     */
    makeProperties(propertyNames) {
        for (let propName of propertyNames) {
            Object.defineProperty(this, propName, {
                get() {
                    return this[`_${propName}`];
                },
                set(value) {
                    this[`_${propName}`] = value;
                }
            });
        }
    }
    /**
     * 名前リストからメソッドを作成
     * 
     * @param {String[]} methodNames   名前リスト
     */
    makeMethods(methodNames) {
        for (let methodName of methodNames) {
            this[methodName] = function () {
                let args = arguments;
                if (this.objectID == null) {
                    return new Promise(async (resolve, reject) => {
                        this.onobjectID = async function () {
                            let rc = await this[methodName].apply(this, args);
                            resolve(rc);
                            return;
                        }
                    });
                } else {
                    let path = `ActiveXObject/${this.objectID}/${methodName}`;
                    return new Promise(async (resolve, reject) => {
                        let json = await ActiveXObject.staticSendMessage('PUT', this.domain, this.port, path, args);
                        // エラーがあれば返す。
                        if (typeof (json['error']) !== 'undefined') {
                            reject(json['error']);
                            return;
                        }
                        //
                        this.SetProperties(json['activeXObject']);
                        //
                        let rc = json['returnValue'];
                        if (rc != null && rc["__constructor_name__"]) {
                            let className = rc["__constructor_name__"];
                            delete rc["__constructor_name__"];
                            let obj = new ActiveXObject(className, rc);
                            resolve(obj);
                            return;
                        }
                        resolve(rc);
                        return;
                    });
                }
            };
        }
    }
    /**
     * 名前リストからプロパティ(コレクション)を作成
     * 
     * @param {String[]} propertyCollectionNames   名前リスト
     */
    makePropertiesCollection(propertyCollections) {
        for (let collectionName in propertyCollections) {
            if (typeof (this[collectionName]) === "undefined") {
                this[collectionName] = {};
            }
            let co = this[collectionName];
            // コレクション本体
            Object.defineProperty(this, collectionName, {
                get() {
                    return this[`_${collectionName}`];
                },
                set(value) {
                    this[`_${collectionName}`] = value;
                }
            });
            // コレクションの設定内容
            let collection = propertyCollections[collectionName];
            // コレクションのプロパティ
            // property
            for (let propertyName of collection.properties) {
                Object.defineProperty(co, propertyName, {
                    get() {
                        return co[`_${propertyName}`];
                    },
                    set(value) {
                        co[`_${propertyName}`] = value;
                    }
                });
            }
            // コレクションのメソッド
            for (let methodName of collection.methods) {
                if (typeof (co[methodName]) === "undefined")
                {
                    co[methodName] = function () {
                        let args = arguments;
                        if (this.objectID == null) {
                            this.onobjectID = async function () {
                                let rc = await co[methodName].apply(co, args);
                                resolve(rc);
                                return;
                            }
                        } else {
                            let path = `ActiveXObject/${this.objectID}/${collectionName}/${methodName}`;
                            return new Promise(async (resolve, reject) => {
                                let json = await ActiveXObject.staticSendMessage('PUT', this.domain, this.port, path, args);
                                // エラーがあれば返す。
                                if (typeof (json['error']) !== 'undefined') {
                                    reject(json['error']);
                                    return;
                                }
                                //
                                this.SetProperties(json['activeXObject']);
                                //
                                let rc = json['returnValue'];
                                if (rc != null && rc["__constructor_name__"]) {
                                    let className = rc["__constructor_name__"];
                                    delete rc["__constructor_name__"];
                                    let obj = new ActiveXObject(className, rc);
                                    resolve(obj);
                                    return;
                                }
                                resolve(rc);
                                return;
                            });
                        }
                    };
                }
            }
            // event 未定
            //
        }
    }

コレクションはプロパティとメソッドのソースをマージした感じで雑になっている。

これで準備が整ったところで、

new ActiveXObject("ADODB.Connection");

すれば、いいはず。

これって、new 演算子をオーバーライドした事になるのかな?(知らんけど

真っ先にプロパティ・チェックするケースは見当たらなかったので、

最初にメソッド呼び出し時に、C#側からobjectIDが送られるまで待って処理する様にしている。

プロパティ参照時もobjectIDが送られるまで待って処理を入れた方がいいけど、

var table_name = schema.Fields["TABLE_NAME"].Value;

の様に呼び出されると、await が入れにくい。

うまくawaitが入っても、絶望的に読みにくくなりそう。

var table_name = await (await (await schema.Fields)["TABLE_NAME"]).Value;



[Hyper-V]The unsigned image’s hash is not allowed

Hyper-Vで第2世代を選択するとUEFIが使えるし、Hyper-VマネージャからのシャットダウンもOK

LinuxのVMを作成して起動すると

暫くまたされた後にUEFIで

The unsigned image’s hash is not allowed

と出て起動に失敗する。

要はセキュアブートの初期設定がWindow用だったダケだった。

セキュアブートを変更

とすると、無事起動できた。

VMを作る時に設定できれば困らないのに・・・



[ActiveX]ブラウザでActiveXを使いたい

EdgeのIEモードもいつまであるのか判らないので・・・

C#のEXEでRESTサービスサーバを作り、ブラウサからデータベースにSQLでアクセスできる様な仕組みをポチポチと作ってみる。勿論、ポート開放なんてしない。

しかし思っていた以上に面倒なことが判明した。

public static object? CreateObject(string progId)
{
Type? t = Type.GetTypeFromProgID(progId);
return t == null ? null : Activator.CreateInstance(t);
}
object? activeXObject = CreateObject("ADODB.Connection");

それっぽいオブジェクトは出来るが、activeXObject変数のクラス名が”System.__ComObject”で、希望するメソッドやプロパティの情報は取得できず、ウォッチビューで中身を見ると[動的ビュー]の中にそれっぽいプロパティが見えるがアクセスする方法は判らなかった。

但し、その変数をそのActiveXObjectのクラスに置き換えると、Accessファイルも開けてしまうのでとりあえず、キャストしてコードを書けば良いらしい。

object? activeXObject
ADODB.Connection? cn = activeXObject;
cn.Open("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=\"C:C:\\Users\・・・\Database1.accd\"");

事前にメソッドの名前さえ判っているなら、動的にメソッドを呼ぶ方法が使える。(カモ

参照:https://dobon.net/vb/dotnet/programing/typeinvokemember.html

string progId="ADODB.Connection";
activeXObject = ActiveX.CreateObject(progId);
// TEST
if (activeXObject != null)
{
    Type t = activeXObject.GetType();
    t.InvokeMember("Open",
        BindingFlags.InvokeMethod,
        null,
        activeXObject,
        new object[] { "Provider=Microsoft.ACE.OLEDB・・・\Database1.accdb\"" });
}
実行すると例外発生
Exception ex:
{"Exception has been thrown by the target of an invocation."}

ん?クラス名がダメなのかな?正しいTypeを取得する方法は予想の斜め上にあった

参照:https://docs.microsoft.com/ja-jp/dotnet/api/system.type.gettype?view=net-6.0#system-type-gettype(system-string)

string progId="ADODB.Connection";
activeXObject = ActiveX.CreateObject(progId);
// TEST
if (activeXObject != null)
{
    Type t = Type.GetType(progId);
    t.InvokeMember("Open",
        BindingFlags.InvokeMethod,
        null,
        activeXObject,
        new object[] { "Provider=Microsoft.ACE.OLEDB・・・\Database1.accdb\"" });
}

これで、メソッドを呼び出すことができたので、キャストだらけのコードも改良の余地がありそうだ。

何でも(メソッド)勘でも(プロパティ)非同期通信で処理すると重そうので、newの後に一式プロパティを送信し、メソッドの実行後に置き換わりそうなプロパティを戻り値を[activeXObject]と[returnValue]に纏めて送信することにする。メソッドを実行するオブジェクトは、ハッシュ管理し、ブラウザからはハッシュを指定して実行する様にした。

※というか

IEの場合、
var table_name = schema.Fields("TABLE_NAME").Value

IE以外の場合、※(…)を[…]に書き換えるしかない様だ。
var table_name = schema.Fields["TABLE_NAME"].Value

な呼び出しをする場合が多い、ここにawait 入れると解読不能になるとしか思えなかった。(ので

※ハッシュ値は諸事象から $”{クラス名}_{ハッシュ値:x}” にした。

javascriptのnewはasyncが指定できないので、newの直後にJavaScript側でプロパティやメソッドの入り口を用意しておく。メソッド実行時にまだハッシュを受信していない場合は、ハッシュ受信時にonobjectIDを処理し、再度メソッド実行を送信するようにする。

※ブラウザ側で不要になったと判断したActiveXObjectオブジェクトは、ActiveXObject.term()を呼び出して、DELETEメソッドでオブジェクトの消去をするように。(できたらいいなぁ

if (this.objectID == null) {
    this.onobjectID = async function () {
        let rc = await this[methodName].apply(this, args);
        resolve(rc);
        return;
     }
} else {
     let path = `ActiveXObject/${this.objectID}/${collectionName}/${methodName}`;
     return new Promise(async (resolve, reject) => {
     let json = await ActiveXObject.staticSendMessage('PUT', this.domain, this.port, path, args);
obj.objectID = json["objectID"];
if (typeof (obj.onobjectID) !== 'undefined')
{
    obj.onobjectID();
}

JavaScript側でActiveXオブジェクトのインスタンスを配列で管理すると使用済みでも残ってしまうので、

インスタンスの管理はC#側のみで行う。

非同期通信の処理待ちをするため await を差し込まないといけないのは面倒だが仕方が無い。

await指定で呼び出す関数の方はasync指定して最後にresolveを渡す様にする。

何気に途中でreturn ; してるケースも同様。

※resolve(true)で処理が途切れるかと思い、手抜きしたら、ダブルresolve(true)してしまい動作が変になったので、直後にreturn を入れておく。

async function xxxxxx () {
・・・・・最後に
Promise.resolve(true);
return;
}

C#側から返す値はJSON形式にしたけど、javascript側からオブジェクトを渡すパラメータはtoStringで済ませているのでちゃんとしないとまずいなぁ。(そのうち何とかしよう

とか、簡単にしようとするとハマるパターンだ。

ちゃんと同期が取れていると

所感)

ブラウザとVisualStudioの両方でブレークポイントを指定して順に動かしていくと、ブラウザとVisualStudioが交互にポップアップして切り替わるのが面白かった。一度お試しあれ。



[ActiveX]JavascriptのActiveXの代案 ※検討中

Windows11でIEが消えていたので、IE用のJavaScriptでActiveXを使ってブラウザでMDBの中身を見るツールが使えない。

そこで、C#でローカルWEB(+REST)サーバEXEを作り、ActiveXをEXEで処理してブラウザは非同期通信でリモートでコントロールさせてみたい。

IE用のindex.htmlとJSファイルはEXEのwwwrootフォルダに入れておき

C#のMainから8000ポートを開き、GETコマンドリクエストがあれば、exeファイルのトコのwwwrootフォルダのファイルを返す様にしておく。

参考文献:簡易Webサーバを実装するには?[2.0のみ、C#、VB]

using System;
using System.IO;
using System.Net;

namespace LocalWebServer
{
class WebFileServer
{
  static void Main()
  {
    string root = @"wwwroot\"; // ドキュメント・ルートは好きな場所でOK
    string prefix = "http://localhost:8000/"; // URLはしっかり書かないとエラる
    // ブラウザでindex.htmlを開く。ココからjsファイルもGETされるハズ
    System.Diagnostics.Process.Start(prefix + "index.html");
  //
    HttpListener listener = new HttpListener();
    listener.Prefixes.Add(prefix); // プレフィックスの登録
    listener.Start();
    while (true) {
      HttpListenerContext context = listener.GetContext();
      HttpListenerRequest req = context.Request;
      HttpListenerResponse res = context.Response;
      Console.WriteLine(req.RawUrl);
      // リクエストされたURLからファイルのパスを求める
      string path = root + req.RawUrl.Replace("/", "\\");
      // ファイルがあれば出力
      if (File.Exists(path)) {
        byte[] content = File.ReadAllBytes(path);
        res.OutputStream.Write(content, 0, content.Length);
      } else {
         // TODO エラー処理 404 あるいはREST処理をしないといけないかもしれない
      }
      res.Close();
    }
  }
}
}

とRESTのWEBサービスっぽく

HTMLファイルは、

<script src="./ActiveX.js">
・・・
<script src="./${他のjsファイル}">

と付け加え、JavaScript上のActiveX(“xxxxx”)の実装を差替える。

ActiveX(objectID) => {
  let promise = new Promise( (resolve, reject) => {
    let domain = "localhost";
    let port = 8000;
    let xhr = new XMLHttpRequest();
    xhr.open('GET', `http://${domain}:${port}/ActiveX/`, true);
    xhr.responseType = 'json';
    xhr.send(null);
    xhr.onload = function(e) {
      if (xhr.readyState == 4) {
        if (xhr.status == 200 ) {
          resolve(JSON.parse(xhr.response));
        }
      }
    };
  });
  return promise;
};

ActiveXの予備元やそのメソッドの呼び出しは全てawaitを追記

async function  xxxx(...)
{
  let obj = await ActiveX("xxxxx");
}

プロパティ呼び出しはC#側でデータを展開しておいた方が良さそう

しかし、メソッドもプロパティもいっぱいあるので

class  ADODB_xxxxxx
    constractor()
    {
        this.className = "ADODB.xxxxxx";
        dummy_properties();
        dummy_methods();
    }
    dummy_properties()
    {
      let properties = ['BOF','EOF',....,'fields'];
      properties.foreach( (p) => {
          this[p] = new Function (`
            set ${p}(v) {
               alert('set ${p} no support.');
            }
            get ${p}() {
               alert('get ${p} no support.');
            }
          `);
      });
    }
    dummy_methods()
    {
      let methods = ['BOF','EOF',....,'fields'];
      methods.foreach( (m) => {
          this[m] = new Function (`
               alert('set ${m} no support.');            
          `);
      });
    }

で、TODO風に作っておいて、後で実装を考えた方が良さそう。

あるいは、各ActiveX用のクラスJSファイルは、ワーカースレッドよろしく

var adodbWorkers = [];
・・・
adodbWorkers["ADODB.xxxxxx"] = new Worker('ADODB.xxxxxx.js');

とやって

adodbWorkers["ADODB.xxxxxx"].postMessage(${メソッド名},${パラメータ1},...,${パラメータn});

onmessage = function(e) {
 switch(e)
 {
   case "BOF":
    どうしよう
     break;
    ・・・
 }
}

adodbWorkers["ADODB.xxxxxx"].onmessage = function(e) {
  何とかかんとか  
 resolve(e,data)とか
}

して、がら空きの実装で使うとこだけ実装する方式で済ませるのがいいかもしれない。



[三国英雄の夜明け]これからどうなるんだろう?

魂玉を7品質にレベルアップするため、全戦力は1600万台に下がってます。

上位キャラ強化 < 下位キャラ弱体化

そんな中、ランク表でナンバー1のキャラが休止に入ったらしい。

ボクより下の方に下がっていたから、探し出すのが大変だった。

どうやら、装備一式や副将を外してる様だ。

とりあえず、ゲームのマップ上は大混戦になっている。

そして今、一番気になるのは!

演武大会で誰にオッズすればいいんだ?

ps.

悩んでいるうちに投票時刻はとっくに過ぎていました。(メデタシメデタシ



【コンピュータのドキュメントとかコードとか】の粒度

フローチャートは砂粒の様な粒度で書けばいいのかもしれないけど、アルゴリズムやワークフレームは大雑把に書いて概要やメソッドのシーケンス(動き)を把握できる方がいい。

面倒なのがワークフローで、UIのテストにも流用できるようとついつい砂粒の様な粒度で書いてしまい、全体がどうなっているのかサッパリ判らなくなり、コードする時に外部(staticっぽい)変数の初期化のタイミングがブレブレで、UIの操作の順(画面1→画面2→画面3とか,画面1→画面3→画面2だったり)で、初期表示で設定する内容がグダグダになりやすい。GUIな画面のテストで操作の順でグダグダになるケースをリストアップするなんて、最悪だ。

例えば、画面1~9へ遷移するボタンがあるメニュー画面を考えてみよう。

このメニュー画面はどういう訳か、ボタンの押す順序でボタンnに対応する画面nの初期状態がバグってしまう事があります。一通り操作して、どんな順序でボタンを押すとバグるのか調べてみましょう。

画面1画面2画面3
画面4画面5画面6
画面7画面8画面9
こんな画面の操作の組合せは何通り?

ここでの「何通り」は、数学で云うところの順列になるので

P9=(9!)÷(0!)=9!=362,880通り

画面を操作してバグのケースを探し出すのは徹夜しても無理っぽく思える。

しかし、これも【ボタン】と云うコントロールを使用している場合であって

画面の座標から画面nを決定するコードをガリガリ書いていたら、画面の全ドットをクリックするテストケースになってしまうので、まだマシ。

今から40年くらい前にロクなライブラリィが無いのにCUIからGUIへ移行した時期のテストは、

「人数をかき集め好き勝手に画面をマウスで叩かせる」≒100人で実施した≒多分大丈夫

の様なMMORPGのαテスト的なシロモノで、テストケースを見積もると桁違いの数になり「テストケースの見積りを諦めていた」のは「今だから云える」話である。

(閑話休題)

さすがに40年も経つと一部の人は経験を積み、

画面nの中で、外部(staticっぽい)変数を書き換える箇所を無くし、テストケースを日常的な業務量ぐらいに削減でき、テストケースの粒度(?)を

メニューの操作、【画面1】の操作、・・・、【画面9】の操作

と大まかに9通りに縮小できる。(それでも中身は相当な数かもしれない

これがうまくいかないと362,880通りの「ボタンを押すダケ」のテストケースがスポーンするので、とても有用である。

また各画面でも、粗相が無い様にコードしないと、地獄を見ることは云うまでもない。

え?そんなの有り得ない?変数のスコープのブロック化や変数をまとめたクラス化や例外処理のTry~Catch~Finallyで解決済み?

だがその常識は40年くらい前から少しづつ確立していったもので未だ未完成である。

Tryブロックをスコープとする変数をCatchやFinallyで参照できないため、Tryブロックの外に変数を配置しなおす(例外処理のスコープの外へ押し出す)ハメになったことは無いかな?

SqlConnection connection= new SqlConnection(DBConnectionString);
try {
    // データベースコネクションを開く
    connection.Open();
    SqlTransaction transaction= connection.BeginTransaction(IsolationLevel.Serializable);
    try {
        // データベースを色々操作してみる
        SqlCommand command1 = connection.CreateCommand();
        command1.CommandText = "SELECT * FROM table001 ORDER BY CategoryID";
        command1.CommandTimeout = 15;
        command1.CommandType = CommandType.Text;
        command1.ExecuteReader();
        ・・・
        SqlCommand command2 = new SqlCommand("INSERT INTO table001(CategoryID) values '001'", transaction.Connection);
        command2.Connection.Open();
        command2.ExecuteNonQuery()
        ・・・
        // 操作を終えたので、データベーストランザクションをコミットする
        transaction.Commit();
    } catch (Exception ex) {
        // 失敗したらしいので、データベーストランザクションを巻き戻す
        transaction.Rollback();
        // 失敗したことを通知
        throw ex;
    } finally {
        transaction.dispose();
    }
} catch (Exception ex) {
    // 失敗したことを通知
    throw ex;
} finally {
    connection.dispose();
}

C#やVBのusingステートメントは自動的にdisposeし変数を始末してくれるので変数を外に出すことは無くなるが、変数がデータベースのトランザクション・オブジェクトの様にシーケンスな手順がある場合にはusingステートメントの中で try~catchを使い適切なシーケンスを維持するべきだろう。

using (SqlConnection connection= new SqlConnection(DBConnectionString)) {
    // データベースコネクションを開く
    connection.Open();
    using (SqlTransaction transaction= connection.BeginTransaction(IsolationLevel.Serializable)) {
        try {
            // データベースを色々操作してみる
            SqlCommand command1 = connection.CreateCommand();
            command1.CommandText = "SELECT * FROM table001 ORDER BY CategoryID";
            command1.CommandTimeout = 15;
            command1.CommandType = CommandType.Text;
            command1.ExecuteReader();
            ・・・
            SqlCommand command2 = new SqlCommand("INSERT INTO table001(CategoryID) values '001'", transaction.Connection);
            command2.Connection.Open();
            command2.ExecuteNonQuery()
            ・・・
            // 操作を終えたので、データベーストランザクションをコミットする
            transaction.Commit();
        } catch (Exception ex) {
            // 失敗したらしいので、データベーストランザクションを巻き戻す
            transaction.Rollback();
            // 失敗したことを通知
            throw ex;
        }
    }
}

transactionがusingステートメントに入り見た目も綺麗なコードになり、transaction.dispose()もthrow exも書かずに済むので大助かり。つまり、usingステートメントとtry~catchは補完関係にある。

しかし、処理の粒度は変わらないから、ちょっと短くなりパッとみ綺麗になっただけ。



【Microsoft365用】Excelのマクロや計算式を除外して保存する方法

【Excel】マクロや計算式を除外して保存する方法 のソースでは、

今のMicrosoft365のExcelでの動作が不安定だったので見直したものです。

Option Explicit

Public Sub アクティブなブックのセル値を値に変換して保存()
On Error GoTo err1
    Call 非表示のワークシートを削除する(ActiveWorkbook)
    Call ワークシートのセル値を値に変換して保存(ActiveWorkbook)
    Call 普通のEXCELファイルに保存(ActiveWorkbook)
exit1:
    Exit Sub
err1:
    MsgBox Err.Description
    GoTo exit1
End Sub

Private Sub 非表示のワークシートを削除する(ByRef ワークブック As Excel.Workbook)
    Dim ワークシート As Excel.Worksheet
    For Each ワークシート In ワークブック.Worksheets
      If ワークシート.Visible <> Excel.XlSheetVisibility.xlSheetVisible Then
        '確認メッセージを非表示にする設定
        Excel.Application.DisplayAlerts = False
        '非表示のワークシートを削除
        ワークシート.Delete
        '確認メッセージを表示する設定
        Excel.Application.DisplayAlerts = True
      End If
    Next
End Sub

Private Sub ワークシートのセル値を値に変換して保存(ByRef ワークブック As Excel.Workbook)
    '全シート選択
    '条件:事前に非表示なワークシートは削除済みであること
    '非表示なワークシートが残っている場合は ワークブック.Sheets(Array("Sheet1", "Sheet2")).Selectな感じで表示ワークシートに限定して選択すること )
    ワークブック.Sheets.Select
    '全ワークシートを選択
    ワークブック.Application.Cells.Select
    '全ワークシートをコピー
    ワークブック.Application.Selection.Copy
    '全ワークシートへ値としてペースト
    ワークブック.Application.Selection.PasteSpecial Paste:=xlPasteValues, _
      Operation:=xlNone, _
      SkipBlanks:=False, _
      Transpose:=False
    'コピペモードの解除
    ワークブック.Application.CutCopyMode = False
    'シート全体を選択したままになっているので、
    'A1のみ選択の状態にする
    Call ワークブック.Application.Cells(1, 1).Select
End Sub

Private Sub 普通のEXCELファイルに保存(ByRef ワークブック As Excel.Workbook)
    Dim ファイル名 As String
    '拡張子を削除 ※削除しないと拡張子が重複する
    ファイル名 = Replace(ワークブック.FullName, ".xlsm", "")
    '確認メッセージを非表示
    ワークブック.Application.DisplayAlerts = False
    '普通のExcelファイルに保存
    ワークブック.SaveAs Filename:=ファイル名, _
      FileFormat:=xlOpenXMLWorkbook, _
      Password:="", _
      WriteResPassword:="", _
      ReadOnlyRecommended:=False, _
      CreateBackup:=False
    '確認メッセージを表示
    Application.DisplayAlerts = True
    '処理完了メッセージ
    MsgBox ファイル名 & ".xlsx" & vbCrLf & "に名前を変えて保存しました"    
End Sub

事前に非表示のシートを削除することで全シート選択の仕方が簡素になってます。非表示シートをそのまま保存したい場合は前の記事を参考にしてください。

Microsoft365のExcelで動作が変だったのは「普通のEXCELを保存」です。

  1. SaveAsのFilenameには拡張子を除くフルパスなファイル名を指定するように変えました。
    • 確認メッセージを非表示にすると、
    • 「chdir パス名」で保存先フォルダを変更する方法が失敗しやすい。
  2. ReadOnlyRecommendedパラメータの名前を訂正
    • 記事にソースをペーストした際に綴りがおかしくなっていたので訂正。
    • ここは旧版でも動かないハズです。

結果的にソースが短くなったのでOKかな。(笑



[Excel VBA]WORD文書のコメントの頁番号を列挙する

今度はWORD文書のコメントの頁番号を列挙するマクロです。

EXCELのワークシートの「コメント行」セル名に「コメント」「コメント対象」「コメント頁番号」の列を追加して使用します。

コメントをワークシートに転記するマクロの内容は赤字頁番号()をコピってこんな感じに

Public Sub コメント字頁番号検索()
On Error GoTo err1
    '初期化
    Dim wordApp As Word.Application
    Set wordApp = CreateObject("Word.Application")
    Dim コメントの開始行 As Long
    コメントの開始行 = ActiveSheet.Range("タイトル行").row + 1
    Dim コメントの列 As Long
    コメントの列 = ActiveSheet.Range("タイトル行").Find("コメント").Column
    Dim コメント対象の列 As Long
    コメント対象の列 = ActiveSheet.Range("タイトル行").Find("コメント対象").Column    
    Dim 頁番号の列 As Long
    On Error Resume Next
    Err.Clear
    頁番号の列 = ActiveSheet.Range("タイトル行").Find("コメント頁番号").Column
    If Err.Number <> 0 Then
        頁番号の列 = ActiveSheet.Range("タイトル行").Find("頁番号").Column
    End If
    '検索対象のWORD文書を開く
    Dim wordFname As String
    wordFname = 検索対象のWORD文書名を調べる
    Dim wordDoc As Word.document
    Set wordDoc = 検索対象のWORD文書を開く(wordApp, wordFname)
    If TypeName(wordDoc) <> "Nothing" Then
        Dim row As Long
        row = コメントの開始行
        wordDoc.ActiveWindow.Selection.Start = 0
        wordDoc.ActiveWindow.Selection.End = 0
        Dim コメント As Word.Comment
        Dim 順番 As Integer
        順番 = 1
        Do While (コメント検索(wordDoc, 順番, コメント))
            Dim コメント対象テキスト As String
            コメント対象テキスト = コメント.Range.text
            コメント対象テキスト = Replace(コメント対象テキスト, vbCr, "")
            コメント対象テキスト = Replace(コメント対象テキスト, vbLf, "")
            コメント対象テキスト = Trim(コメント対象テキスト)
            If コメント対象テキスト <> "" Then
                'コメント
                ActiveSheet.Cells(row, コメントの列).Value = コメント.Range.text
                'コメントの対象テキスト
                コメント.Scope.Copy
                ActiveSheet.Cells(row, コメント対象の列).Select
                ActiveSheet.Paste
                Dim pasteRows As Integer
                pasteRows = Selection.Count
                Dim st, ed
                st = コメント.Scope.Start
                ed = コメント.Scope.End
                Dim 開始頁番号 As Integer
                wordDoc.ActiveWindow.Selection.Start = st
                wordDoc.ActiveWindow.Selection.End = st
                開始頁番号 = wordDoc.ActiveWindow.Selection.Information(wdActiveEndAdjustedPageNumber)
                Dim 終了頁番号 As Integer
                wordDoc.ActiveWindow.Selection.Start = ed
                wordDoc.ActiveWindow.Selection.End = ed
                終了頁番号 = wordDoc.ActiveWindow.Selection.Information(wdActiveEndAdjustedPageNumber)
                '右セルに頁番号を書き込む
                Dim 頁番号説明文 As String
                If 開始頁番号 <> 終了頁番号 Then
                    ActiveSheet.Cells(row, 頁番号の列).Value = 開始頁番号 & "~" & 終了頁番号 & "頁"
                Else
                    ActiveSheet.Cells(row, 頁番号の列).Value = 開始頁番号 & "頁"
                End If
                row = row + pasteRows
            End If
        Loop
    End If
exit1:
    Call MSWORDをそのまま閉じる(wordApp)
    Exit Sub
err1:
    MsgBox Err.Description
    GoTo exit1
End Sub

コメント検索は、順序の順にコメントを引き渡し、無くなったらFalseを返す様にしました。

パラメータのByRef指定を使って順序とコメントの内容を更新する方法は余り使わなくなった手法かもしれません。

Private Function コメント検索(ByRef wordDoc As Word.document, ByRef 順番 As Integer, ByRef コメント As Word.Comment) As Boolean
    With wordDoc.Comments
        If 順番 > .Count Then
            コメント検索 = False
            Exit Function
        End If
        Set コメント = wordDoc.Comments(順番)
        順番 = 順番 + 1
    End With
    コメント検索 = True
End Function

実際には、コメント検索やワークシートに【返信(Replies)】と【解決(Done)】の処理があった方が使い道がありそうですけどね。




top