変奏現実

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

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

インターネット

[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が交互にポップアップして切り替わるのが面白かった。一度お試しあれ。



[Oracle19c]Linux版

AlmaLinux9やOracleLinux9のように新しいOSでは

アレが無い、コレが無いと、dnfがお得意の関連パッケージの自動インストール機能がうまく機能していない。

しかし、古いCentOS7なら、あまり面倒なことをせずに19Cのインストーラの初期画面まで進んだ。

※但し

  • 前提条件のチェックを全てクリアするために、
  • CPU4個、メモリ16GB、スワップ領域16GBに増量。
    • パッケージのアップデート時にX-Windowの画面が固まり操作不能になるので、CPU+3個
    • インスト時にスワップ領域不足の警告が出るのでスワップ領域を増量。
      • 8GBではstartup時にシェアメモリ不足(ORA-27104)が発生。更に増量(16GB)
    • インスト時にシェアメモリ不足の警告が出るのでメモリ増量してカーネルパラメータを再計算
  • リスナー作成直後に再起動するとリスナが起動してないので
    • /etc/hostsにノード名が無かったので追記し、lsnrctl startする。
  • アドバンスモードではOracle Enterprise Manager のポートに何を指定してもダメだったので、チェックを外した。

参考1:CentOS7 Stream Oracle Database 19c : インストール では日本語でインストーラが動いた。

CentOS8は OSのサポートが2021年12月末で終了し、https://ftp.riken.jp/Linux/centos/8.5.2111/isos/x86_64/ のリンクも無くなったので、

CentOS8 Streamを使う事になる。

CentOS Stream 8 へ https://ftp.riken.jp/Linux/centos/8-stream/isos/x86_64/

参考2-1:Oracle Dtabase 19c(CentOS8) は、最終的には「なぜかうまくいかない」。

※参考2-1は、メモリ8GBで試行している。

以下、変更点

1.カーネルパラメータの設定方法

元々は固定値で設定していたが、割り当てたメモリ量から計算する様に修正。

# MEMTOTAL=$(free -b | sed -n '2p' | awk '{print $2}')
# SHMMAX=$(expr $MEMTOTAL / 2)
# SHMMNI=4096
# PAGESIZE=$(getconf PAGE_SIZE)
# cat > /etc/sysctl.d/50-oracle.conf << EOF
fs.aio-max-nr = 1048576
fs.file-max = 6815744
kernel.shmmax = $SHMMAX
kernel.shmall = $(expr \( $SHMMAX / $PAGESIZE \) \* \( $SHMMNI / 16 \))
kernel.shmmni = $SHMMNI
kernel.sem = 250 32000 100 128
kernel.panic_on_oops = 1
net.ipv4.ip_local_port_range = 9000 65500
net.core.rmem_default = 262144
net.core.rmem_max = 4194304
net.core.wmem_default = 262144
net.core.wmem_max = 1048576
EOF

2.インストールするパッケージの追加

インストーラを起動するとエラーが出たので、それっぽいパッケージを追加。

# dnf -y install libnsl

ダウンロードしたrpmをインストールする手順が抜けていたので追加

# dnf localinstall  *.rpm       ※個別にrpmファイルをしてして実行したけど、多分これでいいはず

差し替えるJDKのパスを

# cp -r /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.322.b06-11.el8.x86_64/jre /u01/app/oracle/product/19.3.0/dbhome_1/jdk/jre

hostsに自分のマシン名が無いので

127.0.0.1   ******.******.****** localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         ******.******.****** localhost localhost.localdomain localhost6 localhost6.localdomain6

ここまでやっても、

「インストール」ボタンを押して7%まで進んだあたりで

「ファイル%filename%が見つかりません」

と出るエラーが出る、「継続」押下で続けるが・・・

100%まで進んるのになぁ
OpenJDKに差し替えて動いたけど、どこかで互換性が無くて失敗したっぽい。
DBCAの構成で失敗してるので、日本語対応のJREに差し替えたせいかもしれない。

※メモリ16GBでの動作は未確認。

参考2ー2:CentOS8 Stream Oracle Database 19c : インストール のページはインスト直前まで。

login: oracle
Password:xxxxxxx

$ vi ~/.bash_profile
# 最終行に追記
umask 022
export ORACLE_BASE=/u01/app/oracle

# インストール用 Dir 作成
$ mkdir database

LANG=Cを付けてインストーラを起動すると真っ白になる。とりあえず、保留。

参考2ー3:CentOS8 Oracle Database 19c : インストール すると日本語文字化けするので

cd database
$ unzip LINUX.X64_193000_db_home.zip
$ export CV_ASSUME_DISTID=RHEL8.2
$ export LANG=C
$ ./runInstaller

CentOS8系では、インストーラのJREの日本語フォントとX-Windowの日本語フォントがうまくマッピングされず日本語モードでは文字化けするが、英語モードでのインストールなら問題無し。

しかし、メモリ8GBで得たシェアメモリの容量ではstartupでシェアメモリの容量不足となり16GBに増量し、カーネルパラメータを再計算したところ、startupに成功した。

CentOS8 stream でメモリ8GBでも使えるカーネルパラメータの計算方法があればいいんだけど。

そんな訳で、CentOS8系は微妙。

参照2-3に参照2-1のJRE差し替えを混ぜるといいのかもしれない。

Hyper-Vの起動画面よりもXRDPでWindowsのリモートデスクトップ越しにインストーラを起動した方がマウスカーソルもオーバーラップするしコピペもできインストールもスムーズ。

・・・

と書いたものの、実際やってみると手順通りにヌルっとインストールできず、ドコかでアハってしまう。

rootでシェル起動してもいい?ってメッセージをちょっと放置したら画面が固まるけど、それはOSアップデートが後追いでやってきたせいな気もする。

yumやdnfのアップデートでおきる僅かな修正で何かがうまくいっていないんだろう。

インストーラのチューニングが攻めすぎなんだろうね。

Linux「9」なんて無理そうだから、ここで調査を終了。

とりあえず、CentOS7がアンパイだ。



[CentOS]どうしよう

開発もサポートも切れるので乗り換えないといけない。

とりあえず、AlmaLinux 9.x に乗り換える予定。

GUIをインストするには

# dnf grouplist

でグループパッケージを探してみるが、GNOME Desktopは無かったので

# dnf groupinstall "SERVER with GUI"

で我慢する。

XRDPをインストしようとしたらパッケージが無いとか散々。

ぐぐってみるとちゃんとAlmaLinux9でのインストページがあった。

epelリポジトリィを追加するのを忘れていたせいか。

無事、リモートデスクトップに繋がったものの、コレジャナイ感が漂う。



[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)とか
}

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



[Oracle12c]コンテナ・データベース

最近のOracleのデータベースのインスタンスを作成する時に「コンテナ・データベース(CDB)として作成する」ができる様だ。

今までのデータベース(区別するためにプラガブル・データベース(PDB)と名前が付いている)を複数個格納できるコンテナなデータベースらしい。

各データベース(CDB,PDB)の接続方法は

  • 新たなコンテナ・データベース(CDB)はローカル接続のみ。
  • 今までのデータベース(PDB)はリモート接続のみに変わる。

ということで、一般ユーザ(アプリを含む)からは、何も変わらない気がするけどね。

ただ、安易にCDBを作成するとPDBの接頭語を指定してpdb1,pdb2,pdb3とか名前が自動的に割り振られてしまうようなので、リモート接続のインスタンス(あるいはサービス名?)は変更しないといけなさそう。

そんな訳で、コンテナDBの管理者に

  • CDBにローカル接続
  • CDBで共通ユーザを作って、ログ収集用とか最適化用とかIMPORT/EXPORT用とかロールを割当る。
    • 共通ユーザ名は「C##またはc##」の接頭語が必要らしい。
  • ALTER SESSION SET CONTAINER = ${PDBの名前}; でPDBを指定して、
  • 各PDBの管理者や一般ユーザのアカウントの登録とロールを設定する。

をしてもらえば、後は概ね今まで通りで良いんじゃないのかな?

各PDBのテーブル登録は各PDBの管理者がリモートできるから便利になるかもかも

後、データベース・サーバの移行は・・・

移行元サーバのデータベースがCDBなら、

  1. 移行先サーバで空のCDBを作成
  2. SQL Developerの接続先に移行先・元サーバを追加
  3. SQL Developerで移行元サーバの移行したいPDBで
    1. 「状態の変更」
    2. 「プラガブル・データベースの切断」
  4. 移行したいPDBをフォルダ毎、移行先サーバへコピー(これが難物)
  5. SQL Developerで移行先サーバで
  6. CDBで「プラガブル・データベースのプラグイン」
    1. アンプラグした時に生成したXMLファイルをPDB名を指定

となるらしい。

EXPORT/IMPORTの方が手順が短いけど、IMPORTで難解なエラーに悩むかもしれない事を考えると、楽そうではある。

最もDBが巨大なファイル(100GBとか)になっていたら、rsyncするか、DB移行アプリでも作って一週間も動かし続けるとかかもしれないけどね。

参考文献1:ユージ&ギョータの実践データベース講座

参考文献2:rsync専用の秘密鍵を使ってサーバ間でrsyncする



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

魂玉を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)】の処理があった方が使い道がありそうですけどね。



[Excel VBA]WORD文書の赤色属性を含む文字の頁番号を列挙する

先の[Excel VBA]WORD文書の赤字の頁番号を列挙するでは、赤字以外の属性(抹消線など)があるテキストをヒットしなかったので、改訂版です。

今回はWORDの「高度な検索」の「書式」で「赤字」だけ設定する方法を使います。

と云っても、赤文字頁番号検索()少し変えたダケです。(笑

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
    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
        Do While (高度な赤字検索(wordDoc))
            Dim 赤字 As String
            赤字 = wordDoc.ActiveWindow.Selection.text
            赤字 = Replace(赤字, vbCr, "")
            赤字 = Replace(赤字, vbLf, "")
            赤字 = Trim(赤字)
            If 赤字 <> "" Then
                wordDoc.ActiveWindow.Selection.Copy
                ActiveSheet.Cells(row, 赤字の列).Select
                ActiveSheet.Paste
                Dim pasteRows As Integer
                pasteRows = Selection.Count
                Dim st, ed
                st = wordDoc.ActiveWindow.Selection.Start
                ed = wordDoc.ActiveWindow.Selection.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

後は、高度な赤字検索()を追加するだけです。

中身はWORDで「高度な検索」の「書式」で「赤字」だけ設定した時のマクロ記録内容を少し調整したものです。

Private Function 高度な赤字検索(wordDoc As Word.document) As Boolean

    With wordDoc.ActiveWindow.Selection.Find
        '書式関連を初期化
        .ClearFormatting
        .Font.Color = wdColorRed
        '文字関連を初期化
        .text = ""
        .Replacement.text = ""
        .Forward = True
        .Wrap = wdFindStop '文書の終わりで中断
        .Format = True
        .MatchCase = False
        .MatchWholeWord = False
        .MatchByte = False
        .MatchAllWordForms = False
        .MatchSoundsLike = False
        .MatchWildcards = False
        .MatchFuzzy = True
        '検索
        .Execute
        '結果を取得
        高度な赤字検索 = .Found
    End With

End Function

検索対象のWORD文書名を調べる検索対象のWORD文書を開くMSWORDをそのまま閉じるは前の記事のままです。

Font.ColorとFont.ColorIndexのいづれを使うのか良いのかは悩ましいです。

後、デバッグ中は起動したMS-WORDをタスクマネージャから終了していましたが、上記のwordAppを外部変数にして、マクロ画面から終了させた方が便利そう。

Dim wordApp As Word.Application
・・・
Set wordApp = CreateObject("Word.Application")
・・・
public sub マクロ実行中に起動したMSWORDを強制終了
    Call MSWORDをそのまま閉じる(wordApp)
End Sub 



top