投稿者「サイト管理者」のアーカイブ

Installer

・・・って言えるのかな?

正直、レジストリは汚したくない。でも、プログラムの動作に必要なユーザーの情報や設定は保存して再利用したい・・・そんな時、役立つのが定義ファイル。

今時、レジストリを使わずに定義ファイル(iniファイル)を使うなんて、完全に時代遅れなのかもしれないが、2つか、3つの設定内容を記録して利用するには、すごく便利なのは事実。ただ、ひとつだけ問題があるとすれば、exeファイルの周辺にユーザーの知らないファイルが生成されること。

【参考】
以前、この問題の解決方法として、パブリックのドキュメント(C:\Users\Public\Documents)に定義ファイル他を保存して、プログラムから利用したこともあった。それがスマートか、どうか、は別にして、それなりに目的は実現できたけど・・・なんか、どこか、すっきりしない感じが残って(毎回コレで行こう!みたいな気持ちになれなかった)。ユーザーに意識させたくない部分を意図的に「隠した」って、自分的には、どうしても思っちゃうからかなー。

今回は、その「困ったこと」を僕なりにどう解決したか? ・・・というお話。

【目次】

1.困ったこと
2.自分的解決策はただ一つ
3.作ってみた①(全自動)
4.作ってみた②(マニュアル)
5.まとめ
6.お願いとお断り

1.困ったこと

iniファイルを使用したり、リソースに埋め込んだDLL、もしくは画像やデータベースその他のファイルをプログラムからexeの周辺に生成して利用する場合、例えばデスクトップにexeファイルを置くと、プログラムの起動と同時に、ユーザーから見て「何、コレ?」みたいなファイル(or フォルダ)が EXE の周辺に出来てしまう。

例えば、次のようにリソースに埋め込んだDLLがインストール先フォルダになければ、それを EXE のある場所に生成する場合がそうだ。

procedure TForm1.FormCreate(Sender: TObject);
var
  dllFileName:string;
begin
  //リソースからDLLを(なければ)生成
  dllFileName:=ExtractFilePath(Application.ExeName)+'XXX.dll';
  //ファイルの存在を確認
  if not FileExists(dllFilename) then
  begin
    //リソースを再生
    with TResourceStream.Create(hInstance, 'Resource_1', RT_RCDATA) do
    begin
      try
        SaveToFile(dllFileName);
      finally
        Free;
      end;
    end;
  end;
end;

プログラムを終了しても、当然、それらはexeの周辺に残っている。これらはユーザーから見れば、突然生まれた不審なファイル(or フォルダ)としか思えなくても不思議はない。

特にデスクトップにiniファイルやDLLを生成するEXEを置いた場合には、キレイ好きなユーザーから見れば、「この画面を汚すEXE、なに?」ってことにもなりかねない。

2.自分的解決策はただ一つ

ユーザーに対して、このような不安を与えないようにするなら、プログラム配布専用のインストールプログラムを作り、まず、そのリソースに配布したいプログラム(EXE)を埋め込む。で、このインストール専用のプログラムを起動したら、例えばユーザーのマイドキュメント内に適切な名前のフォルダを作成して、そこにexeをリソースから生成してコピー。最後に、そのEXEへのショートカットをデスクトップに自動的に作る・・・みたいなインストール専用のプログラム(=Installer)を書けばいいのかな? ・・・って。

こうしておけば、ユーザーはデスクトップのショートカットをダブルクリックするだけでプログラムを使えるし、ユーザーに見せたくないプログラムの動作に必要な情報も、その存在を隠しながら、マイドキュメント等に作った専用フォルダ内に生成できるはず。

3.作ってみた①(全自動)

予め、リソースにインストールしたい完成した配布用EXEを埋め込んでおく。DelphiのIDEの「プロジェクト」→「リソースと画像」の順にクリックして、埋め込むEXEを指定。

埋め込むEXEは、後の混乱を避ける意味でも、このインストールプログラムのプロジェクトフォルダに「Resource」等の専用フォルダを作成して、そこに完成した配布用EXEをコピーしておき、それを指定するのが方法的には Best かと。

このEXEの中には、当該プログラムの動作に必要なDLL等が全て埋め込まれている

GUIは、こんな感じで作成(実行時の画面)。

基本的に「全自動でインストール」内のボタン1ClickでOK!(の予定)

わかりやすい、とか、わかりにくい、とか、そういう問題とは別に、Enterキーひと押しで完全に動作すれば、インストールプログラムのインターフェイスの良し悪しは、特に問題にならないはず。

で、「マイドキュメントに専用~」ボタンをクリックした時の手続きは次の通り。

  private
    { Private 宣言 }
    Setup_FolderPath:string;
    Setup_ExeName:string;

implementation

{$R *.dfm}

uses
  Winapi.ShlObj, Vcl.FileCtrl, System.UITypes, plShortcutUtils;

  //ShlObjはSHGetKnownFolderPath関数を使用するために追加
  //ShellExecute関数を使用してフォルダを開いて表示する場合はWinapi.ShellAPIも追加する

  //Vcl.FileCtrlは、新しいフォルダ作成ボタン付きフォルダの選択ダイアログの表示に必要

procedure TForm1.btnAutoClick(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
  rsFileName:string;
  LDir:String;
begin

  //マイドキュメントフォルダへのPathを取得する
  FolderID:=StringToGUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}');
  if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
  begin
    Setup_FolderPath := FolderPath;
  end;

  //インストール先フォルダの有無を調査->なければ作成
  if not System.SysUtils.DirectoryExists(ExtractFileDir(Setup_FolderPath+'\'+Setup_ExeName+'\')) then
  begin
    //フォルダ階層を作成
    System.SysUtils.ForceDirectories(ExtractFileDir(Setup_FolderPath+
      '\'+Setup_ExeName+'\'));
  end;

  //Path
  rsFileName:=Setup_FolderPath+'\'+Setup_ExeName+'\'+Setup_ExeName+'.exe';

  //ファイルがある場合は削除
  if FileExists(rsFilename) then
  begin
   //ファイルが存在したときの処理
    DeleteFile(rsfileName);
  end;

  //リソースを再生
  with TResourceStream.Create(hInstance, 'Resource_1', RT_RCDATA) do
  begin
    try
      SaveToFile(rsFileName);
    finally
      Free;
    end;
  end;

  //デスクトップにこのプログラムのショートカットを作成
  if CheckCreateShortCut.Checked then
  begin
    //plShortcutUtilsユニット内の関数類を使用
    //CSIDL_DESKTOP等の定数名の使用にはusesにShlObjが必要
    //CSIDLの値からフルパスを取得
    //ショートカットを作成する場所
    LDir := GetDirectoryFromCSIDL(CSIDL_DESKTOP);

    if CreateShortCutLink(rsFileName, LDir, Setup_ExeName) then begin
      //ショートカットの作成場所によっては,以下のコードで更新が必要
      //SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0);
    end;

    MessageDlg('Done!', mtInformation, [mbOk] , 0);
  end;

end;

ショートカットの作成方法は、Mr.XRAYさんのWebページにある方法をコピペしました。

880_ショートカットの作成と削除

http://mrxray.on.coocan.jp/Delphi/plSamples/880_CreateShortcut.htm

Private 宣言した Setup_FolderPath には、FormCreate手続きで次のようにして(初期表示のため、取り敢えず)マイドキュメントフォルダへのPathを入れておきます・・・。

procedure TForm1.FormCreate(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
begin

  //インストールするEXEの名前
  Setup_ExeName:=EditExeName.Text;

  //マイドキュメントフォルダへのPathを取得する
  FolderID:=StringToGUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}');

  if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
  begin
    Setup_FolderPath := FolderPath;
    EditPath.Text:= Setup_FolderPath;
  end;

end;

それから、インストールするExeの名前はForm上で非表示のGUI(EditExeName.Text)に設定しています(FormCreate時にグローバル変数に名称を読み込んで利用)。

こうしておけば、リソースに組み込むExeファイルを変更した時も、InstallするExeの名称を変更するだけで、このインストールプログラムを使えます。

設計時の画面左下に、実行時には非表示のLabelとEditコントロールを配置。このEditコントロールのTextプロパティにインストールするExeの名称を設定。

InstallするExeの名称Labelとその右のEditコントロールのVisibleプロパティはFalse

動作を検証した結果、プログラムは期待通りに動作しました。
ただ、32bitバージョンを作成した際に、実行形式ファイルを作成出来なくなるエラーが何回かありましたが・・・(原因がよくわかりません)。

4.作ってみた②(マニュアル)

もし、ユーザーが「おまかせインストール」ではなく、「フォルダを指定してインストール」の方を選択した場合の「ルートディレクトリの指定」に関する手続きは・・・

procedure TForm1.RadioGroup1Click(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
begin

  case RadioGroup1.ItemIndex of
    0:begin
      //マイドキュメントフォルダへのPathを取得する
      FolderID:=StringToGUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}');
      if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
      begin
        Setup_FolderPath := FolderPath;
        EditPath.Text:= Setup_FolderPath;
      end;
    end;
    1:begin
      //マイコンピュータへのPathを取得する
      Setup_FolderPath := 'C:\';
      EditPath.Text:= Setup_FolderPath;
    end;
  end;

end;

ちなみに、PCを選択した場合に表示される「フォルダーの参照」ダイアログは・・・

PCのフォルダ構成に詳しい人向きの表示になります・・・

で、インストール先を選ぶ「変更」ボタンをクリックした際の挙動は・・・

procedure TForm1.btnGetPathClick(Sender: TObject);
var
  SelectDir: String;
begin

  case RadioGroup1.ItemIndex of
    0:begin
      //フォルダを選択 -> MyDocumentsを指定
      //if SelectDirectory('', '::' + GUIDToString(CLSID_MyDocuments), SelectDir) then

      //MyDocumentsを指定 -> MyDocumentsを指定 & 新しいフォルダ作成ボタン付き
      if SelectDirectory('', '::' + GUIDToString(CLSID_MyDocuments), SelectDir,
        [sdNewUI, sdNewFolder, sdShowEdit], Self) then
      begin
        EditPath.Text:=SelectDir;
        Setup_FolderPath:=EditPath.Text;
      end;
    end;
    1:begin
      //フォルダを選択 -> を指定
      //if SelectDirectory('', '::' + GUIDToString(CLSID_MyComputer), SelectDir) then

      //MyMyComputerを指定 -> MyMyComputerを指定 & 新しいフォルダ作成ボタン付き
      if SelectDirectory('', '::' + GUIDToString(CLSID_MyComputer), SelectDir,
        [sdNewUI, sdNewFolder, sdShowEdit], Self) then
      begin
        EditPath.Text:=SelectDir;
        Setup_FolderPath:=EditPath.Text;
      end;
    end;
  end;

end;

上の手続きで使用しているGUIDToString関数の引数CLSID_XXXには、その種類に制限があるようです。ShlObj.pas内のGUID定義を見てみると・・・

const
  CLSID_NetworkDomain: TGUID     = '{46E06680-4BF0-11D1-83EE-00A0C90DC849}';
  {$EXTERNALSYM CLSID_NetworkDomain}
  CLSID_NetworkServer: TGUID     = '{C0542A90-4BF0-11D1-83EE-00A0C90DC849}';
  {$EXTERNALSYM CLSID_NetworkServer}
  CLSID_NetworkShare: TGUID      = '{54A754C0-4BF0-11D1-83EE-00A0C90DC849}';
  {$EXTERNALSYM CLSID_NetworkShare}
  CLSID_MyComputer: TGUID        = '{20D04FE0-3AEA-1069-A2D8-08002B30309D}';
  {$EXTERNALSYM CLSID_MyComputer}
  CLSID_Internet: TGUID          = '{871C5380-42A0-1069-A2EA-08002B30309D}';
  {$EXTERNALSYM CLSID_Internet}
  CLSID_RecycleBin: TGUID        = '{645FF040-5081-101B-9F08-00AA002F954E}';
  {$EXTERNALSYM CLSID_RecycleBin}
  CLSID_ControlPanel: TGUID      = '{21EC2020-3AEA-1069-A2DD-08002B30309D}';
  {$EXTERNALSYM CLSID_ControlPanel}
  CLSID_Printers: TGUID          = '{2227A280-3AEA-1069-A2DE-08002B30309D}';
  {$EXTERNALSYM CLSID_Printers}
  CLSID_MyDocuments: TGUID       = '{450D8FBA-AD25-11D0-98A8-0800361B1103}';
  {$EXTERNALSYM CLSID_MyDocuments}

自分的に使いたいなーって思う定義は、MyComputerとMyDocumentsぐらいしかありません(Desktopがない!)。まぁ、ない袖は振れない・・・ということでしょう。

どうしてもデスクトップを指定したい場合は、上で使用した GetDirectoryFromCSIDL(CSIDL_DESKTOP) のように、CLSID_XXX ではなく、CSIDL_XXX を使える形式に書き直す必要がありそうです(今回は、書き換えずに進めることにします)。

で、「実行」ボタンの挙動は、ほとんど再掲ですが・・・

procedure TForm1.btnOKClick(Sender: TObject);
var
  rsFileName:string;
  LDir:String;
begin

  //Path
  rsFileName:=Setup_FolderPath+'\'+Setup_ExeName+'.exe';

  //ファイルがある場合は削除
  if FileExists(rsFilename) then
  begin
   //ファイルが存在したときの処理
    DeleteFile(rsfileName);
  end;

  //リソースを再生
  with TResourceStream.Create(hInstance, 'Resource_1', RT_RCDATA) do
  begin
    try
      SaveToFile(rsFileName);
      //MessageDlg('Generate!', mtInformation, [mbOk] , 0);
    finally
      Free;
    end;
  end;

  //デスクトップにこのプログラムのショートカットを作成
  if CheckCreateShortCut.Checked then
  begin
    //plShortcutUtilsユニット内の関数類を使用
    //CSIDL_DESKTOP等の定数名の使用にはusesにShlObjが必要
    //CSIDLの値からフルパスを取得
    //ショートカットを作成する場所
    LDir := GetDirectoryFromCSIDL(CSIDL_DESKTOP);

    if CreateShortCutLink(rsFileName, LDir, Setup_ExeName) then begin
      //ショートカットの作成場所によっては,以下のコードで更新が必要
      //SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0);
    end;

    MessageDlg('Done!', mtInformation, [mbOk] , 0);
  end;

end;

案外簡単に、思った通りのインストールプログラムが作れました!

ふと疑問に思い、今回、調べて初めて知ったのですが、「インストール」と「セットアップ」は意味的に異なるようです。

セットアップはよく、「インストール」と同義語として解説されることもありますが、インストールは、ソフトウェアを動かすためのプログラムやデータなどの各種ファイルをコンピュータにコピーすることであり、セットアップは、インストール後に自分のコンピュータに合わせて必要な設定をすることまでを指す言葉です。

ネット用語辞典(https://bb-navi.jp/netjiten/sa25.html)より引用

5.まとめ

iniファイルを使用したり、リソースに埋め込んだDLLその他のファイルをインストール先フォルダ等に生成して使うようなプログラムを配布する場合、ユーザーに優しいプログラムとするため、必要のないファイルその他を見せない工夫があった方がよいのではないか? と思い、マイドキュメントフォルダ等に専用フォルダを作成して、そこへExeをインストールするプログラムを書いてみた。

これまでユーザーのPCに手作業でEXEのインストール作業を行ってきたが、このようなインストーラにExeを埋め込んで配布すれば、その作業がいらなくなる?

取り敢えず、現場で運用して見ます!

6.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

サインイン 4 アプリからオンラインのOneDriveを表示

これまで「サインイン」と題して、オンラインのOneDriveへ、いかに楽してサインインするか・・・という内容の記事を3つ書いた。

それはアプリのOneDriveから、簡単にオンラインのOneDriveを表示する方法がわからなかったから。で、ことここに至ってようやく、その方法を発見。

結局、これまでの全ては、オンラインのOneDriveへ「いかに苦労してサインインするか」に変わった気が・・・。

【目次】

1.アプリからオンラインのOneDriveを表示
2.アカウントの切り替えも簡単
3.まとめ
4.お願いとお断り

1.アプリからオンラインのOneDriveを表示

なんで今まで気がつかなかったんだろう・・・。アプリのOneDriveの「フォルダーを開く」の右隣にオンラインのOneDriveを表示する「オンラインで表示」があった!

こんなコマンドがあったなんて・・・ちっとも気がつきませんでした。

さらに、アプリのOneDriveの「フォルダーを開く」で表示されるエクスプローラーの右上の「同期しています」をクリックすると表示されるサブメニューの右下にも「オンラインで表示」が存在!(ここのキャプションは、その時々の状況を伝えるほかの文字列「例:エラー」等になることもあるようだ)

ここにも「オンラインで表示」があった!

いずれもクリックすると、Webブラウザが起動してオンラインのOneDriveが表示される。

データ交換用のUSBメモリのようにオンラインのOneDriveを使用したい時は、このWebブラウザに表示されたオンラインのOneDriveへ、必要なデータをアップロードして、別のPCで同様にオンラインのOneDriveにサインインして、必要なデータをダウンロードすればいい。

オンラインのOneDriveへデータをアップロード

追記(20230829)

回線速度とは別に、使用するWebブラウザによりダウンロード速度に違いが生じることがあるのだろうか? 昨日、150MB程度のZipファイルをOneDriveからダウンロードしたのだが、Myプログラムから実行したそれは「あまりにも」遅く、耐え難かったので、ChromeからOneDriveに接続してダウンロードしてみたら「ものすごく」速かった・・・です。

遅かったのはTWebBroeserを使ったプログラムだったので、TEdgeBrowserに変更した新しいプログラムで試してみたら、ChromeでOneDriveに接続した場合と変わらないダウンロード速度で快適に作業できました!

2.アカウントの切り替えも簡単

個人のアカウントから、組織のアカウントへ(もちろん、その逆も)の切り替えも簡単でした。オンラインで表示したOneDrive画面右上のアカウントマネージャーをクリックして切り替えたいアカウントを選択するだけです。

アカウントの切り替え画面

3.まとめ

(1)アプリのOneDriveからオンライン表示への切り替えは走召簡単(泣)
(2)複数のアカウントがある場合、アカウントマネージャーで切り替え可能
(3)間違った思い込みは無駄な苦労の元。アプリの使い方をよく勉強しよう!

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

サインイン 3

追記(20230827 OneDriveアプリからオンライン表示へ切り替え)

無駄にプログラムなんか書く必要はありませんでした!

以下、『いかに苦労してOneDriveにサインインするか』という、上記サインイン 4に辿り着くまでの、長いながいまわり道の記録です。なので、お読みいただく価値がないことを最初に申し添えます。m(__)m

この記事は、アプリとして実行(タスクトレイに常駐)するOneDriveではなく、Web上のOneDriveへ直接データをアップロードし、別のPCでそのデータをダウンロードする、言わばデータ交換用USBメモリのようにOneDriveを使用する方法の一例です。PC内のOneDriveフォルダにあるデータと、クラウド上のOneDriveにあるデータの同期などは、まったく考慮しておりませんので、その点にはどうかご注意願います。

プロローグ

2023年8月のある日を境に、OneDriveの挙動が変わったことに気づいた。組織アカウントと個人アカウントの両方で同一ID(メールアドレス)を使用している場合、個人アカウントとしてサインインしようとすると、個人アカウント用のサインイン画面が新たに表示され、その都度、パスワードの入力が必須になったようだ・・・。

1.あれは夢だったのか・・・?
2.IDの入力を2回求められるようになった・・・
3.イロイロ調べてみた!
4.パスワードも自動入力!
5.画面の表示設定
6.まとめ
7.お願いとお断り

1.あれは夢だったのか・・・?

これまでに、過去の記事として「サインイン」、「サインイン 2」と題し、クラウド上のOneDriveへ、いかに「楽して入るか」ということについて自分なりに工夫した点のまとめの記事を(プログラミングの一つの区切りでもあったし)掲載した。

実行形式ファイルを配布していないから、動作の確認のしようがないじゃないか・・・という声が聞こえてくるような気もするけど、exeの配布が僕の目的ではなく、夢中になったことのプログラミング記録を残すことがこのBlogを書く目的なので、そこは悪しからずご了解ください(バグに満ちた?実行形式ファイルを配って、多くの方にご迷惑をおかけしたくないという思いも当然あります)。

「サインイン 2」で思った通りのプログラムが完成して、「使うぞー!」という段階に入った8月中旬、これまでと異なる挙動をWebブラウザが示すことに僕は気づいた。

サインイン2で作成した接続専用プログラムを起動し、IDを自動入力して「次へ」をクリックして「個人用アカウント」をクリックしてもOneDriveに素直に入れないのだ・・・。例外なく新しい画面でIDの入力を再度求められ、さらに毎回パスワードも入力しなければならない。

今まではこんなことなかったのに・・・
IDを入力するだけで入れた、あのOneDriveは夢だったのか・・・?

2.IDの入力を2回求められるようになった・・・

具体的にはどうなるのか、画面をつけて説明すると以下の通り。
FormCreate手続きの最後で、次のようにOneDriveのサインイン画面を呼び出して・・・IDとして利用しているメールアドレスを自動入力。

  //Navigate
  EdgeBrowser1.Navigate('https://onedrive.live.com/about/ja-jp/signin/');
IDとして利用するメールアドレスを入力して「次へ」をクリック(もちろん入力は自動化)

すると、次の画面が表示されて、これまでなら「個人用アカウント」をクリックするだけでOneDriveにサインインできた(過去にパスワードを入力してサインインに成功していればそのCookieが残っているから?)。

過去(90日間以内?)にサインインに成功していれば、パスワード入力は必要なかった・・・はず。
この画面が今回の問題の根本的な原因がここにあることを示唆している(気がする)。

ところが、2023年8月中旬(頃?)からは、「個人用アカウント」をクリックすると、なんと・・・(おそらく、ここでサインインするアカウントが「個人用アカウント」であることがユーザーによって確定されたということで、Microsoftさん的には、今度は安心して・・・再度、個人アカウント専用のOneDriveへのサインイン画面を表示して、サインインしてください・・・という意味なのだと思いますが)

サインインの最初の画面に戻ってしまう・・・ 感覚的には、ウソだろ、なんで? って感じ。

「戻ってしまう・・・」と書いたが、正確にはカーソルのキャレットの点滅位置(スクリーン座標)が異なっていることと、その下に「サインイン オプション」なる最初の画面にはない表示があることから気づいたのだが、(最初に表示されたのとは)「別」の(=個人用アカウントの?)サインイン画面が表示されるのだ(サインイン画面のURLも確認したが、当然最初のサインイン画面とは異なっている)。

IDとして利用するメールアドレスは、この段階では疑似クリック&貼り付け失敗に備えてクリップボード上に送ってあるから、キャレットの点滅を確認し、Ctrl+Vして入力欄に貼り付けて「次へ」をクリックすると、さらに、これまでは出てこなかったパスワードの入力画面が表示される・・・。

Cookieの存在など忘れたかのようだ

これまで利用していたはずのCookieは何処へ消えたのか・・・不思議に思いながら、パスワードを入力してサインインボタンをクリックすると、やっとOneDriveに入れる・・・。

しかも、最後に表示される画面で、「今後このメッセージを表示しない」をチェックして「はい」を選択(クリック)しても、このメッセージは毎回必ず表示される・・・つまり、個人用アカウントでサインインした場合は、「サインインの状態は維持されない」。

My 環境では、「はい」をクリックしても設定は維持されない

拝啓 マイクロソフト様

オレみたいな輩がいるから、こんな仕様になったんですか?

Webブラウザが見えないところでやってることなんて、何にもわかりません。わからないけど・・・

「ボクのお父さんは、桃太郎というやつに殺されました。」

あの手紙を読んだときと同じくらいショックでした。

悪いことをするつもりはまったくありません。

いつも使ってるID(=メールアドレス)で、

OneDriveに楽して入りたいだけなんです。

「それが大きな間違いだ・・・」と言われたら、素直に「はい」と言うしかありませんが・・・

3.イロイロ調べてみた!

(期待したことなど一度もないが)これまで通りの七転八倒の結末に、今回も大いに落ち込む。が、唯一の救いは「OneDriveにサインインできなくなったわけではない」という部分だ。設定が変わって、セキュリティがより厳しくなった・・・というか、組織アカウントと個人アカウントの区分がより厳密になった・・・と言えばいいのかな? この問題の全体像は、多分、僕には把握できないだろうけれど、とりあえず、僕がわかるところまでOneDriveへサインインする仕組みについて調べてみることにした。

その結果、いちばんわかりやすかったのが、こちらの記事。

『サインインの状態を維持しますか ?』のオン/オフをユーザーごとに制御する

https://itbeginner.tech/2020/07/25/keep-me-signed-in/

上記Webサイト様の記事によれば、「有効期限が切れるシナリオ以外に、サインイン画面が表示される代表的な例」は次の5つがあるとのこと。

  • ユーザーのパスワードが変更されている
  • サインインの際に prompt=login パラメーターが付与されている
  • 多要素認証 (MFA) を実施する必要がある
  • inPrivate モードのブラウザでサインインしている
  • ブラウザが Cookie を保存できない、送信できない … など。

https://itbeginner.tech/2020/07/25/keep-me-signed-in/より引用

パスワードは変更してないから、それ以外の4つのうち、個人用アカウントでサインインする場合には、どの理由が該当するのか(自分的には、組織アカウントと個人のアカウントの両方に登録されているIDが使用された場合に、どちらのアカウントでのサインインであるかを確定することがサインイン画面が2回表示される最大の目的だと思うのだけれど)、いずれにしても原因がはっきりわかっても、そこから先が独力では解決できそうにありません。

おそらく、組織アカウントと個人アカウントを明確に切り分けない限り、現段階でこの問題の解決策はないように思えてきました。

また、上記Webサイト様の記事では『Fiddler』(フィドラー?)というHTTPS通信を解析するソフトが紹介されており、記事を読んで(僕には絶対に結果を上手く扱えない・・・)と直感的に思ったけれど、取り敢えずLink先へ飛んでプログラムをダウンロードしてMy PCにインストール。動かしてみた結果が次の通り。

HTTPS通信の内容(My IDやPW)が表示されてる・・・ すごいー!!

『Fiddler』のインストールと使い方は、次の記事を参照して行いました!

HTTPS パケット キャプチャ ツール Fiddler のインストールから使用開始まで。

https://qiita.com/Shinya-Yamaguchi/items/37347ec532824c2dccad

で、せっかくインストールして動かしてみた『Fiddler』ですが、この『Fiddler』が表示してくれているHTTPS通信の内容を、Delphi の Object Pascal で書く OneDrive への接続プログラムで活用する方法がわかりません・・・。残念ながら僕には、現時点でそれだけのプログラミングスキルが・・・悔しいけれどありません。それをイチから学ぶには、とんでもない時間がかかりそうです・・・

もはやこれまで・・・
あきらめるしかないかぁ・・・

っと、思ったところで気がつきました!

何をあきらめるというのだろう?
Cookieを利用した形でのパスワード入力を回避できないなら、
パスワードも自前で暗号化して定義ファイルに保存しといて、
自動入力すればイイだけのことじゃないか・・・

サインイン画面が再び表示されたらCtrl+Vで、クリップボードにあるIDのデータを貼ればいいだけだし、さらに続けてパスワード入力が要求される場面があっても、僕のプログラム側で対応して、ID入力同様にパスワード入力を半自動化してしまえばいい。

負け惜しみじゃなく、すべてを手入力するよりか、はるかにラクだ!
貼り付けのショートカットだって、Ctrl+Vだけなら覚えて貰えるはず・・・
目の前に見えてるボタンのクリックなら、なんの問題もない。

要は、困った時のサポートと、「慣れ」だ。

OneDriveにサインインする「敷居」さえ、もっと低くできれば・・・
みんなに やさしい プログラムになる。

風邪などのウイルス性疾患全般に効く特効薬はないみたいだけれど、たとえウイルスは退治できなくでも、ウイルスの引き起こす様々な症状への対症療法ならたくさんある。

それと同じように、Windows Hello や Cookie を利用してパスワード入力そのものを回避するというような根本的な問題解決は(今の僕には)出来ないけれど、サインインのID入力画面が表示されたら、IDを自動入力、クリックで進めるところは素直にクリックして次へ進み、もしパスワード入力画面が表示されたら、そこでまたパスワードを半自動入力するという、いわば対症療法的な方法で少しでも「楽に」サインインする方法が実現できるよう頑張ればいい。それだって、IDやパスワードを毎回全部手入力するよりは、ずっとラクなはずだ・・・。

繰り返せば、やがて「慣れ」という免疫ができる・・・。

それに、実験してて気がついたんだけど、自動的にCookieが適用?されて、パスワード入力を求められない(パスワード入力の画面が表示されない)IDもあるようだ。

詳しくは書かないけど、サインイン後の画面そのものがIDによって・・・違う。

なんでIDにより、Webブラウザの挙動が異なるのか?

おそらく、僕がOneDriveへのサインイン時に、IDとして使用しているメールアドレスは、Office 365の申し込み時にもその登録に使用したから当然Azure ADアカウント(=職場や学校のアカウント:組織アカウント)になっていて、さらに、同じメールアドレスが昔のLive IDつながりのMicrosoftアカウント(=個人のアカウント)としても登録されているから、このアカウントの二重登録状態をなんとか解消したい(させたい)Microsoft社の意向があって、こういうことになっているんだと思うんだけど・・・。

そうか、組織アカウントなら・・・。

ただ、僕のように、個人のアカウントのメールアドレスをどうしても変更したくない場合は、どうしたらよいのだろう?

そのへんの違いと仕組みは、これからの成長課題としておいて・・・、今は、今の僕に出来るいちばんイイことをしよう!

4.パスワードも自動入力!

さっそく、次のようにGUIを修正。

ID=メールアドレス入力用のGUIはそのまま利用(前回、作成したもの)

上のGUIの「待ち時間:1500(ミリ秒)」ComboBoxの右隣りに下のGUIを追加。

パスワード入力用のGUIを追加

上のように、パスワードをマスクするには、次のように設定。

パスワードを入力させるとき、入力した文字が他人に見られないように*などを表示(現在は黒丸●が標準?)するには、PasswordCharプロパティに * を設定するだけでOK!

//Password入力用文字列に'*'を設定
Edit1.PasswordChar:='*';

//Password入力用文字列設定を解除(''で#0を囲まないこと!)
Edit1.PasswordChar := #0;

【注意】
Editコントロールのプロパティで直接指定する場合は、アスタリスクをシングルクオートで囲んで ‘*’ としないこと! 「プロパティ値が違います」と即エラーになる。

シングルクオート囲みなし、単に #0 or * を入力すればOK!

マスク解除のプロパティでの指定例

パスワードのマスクを、CheckBoxのチェックと連動させるのであれば・・・

「確認」チェックボックスのチェックに連動してマスク状態が変化する
procedure TForm1.chkPWClick(Sender: TObject);
begin
  if chkPW.Checked then
  begin
    EditPW.PasswordChar := #0;
  end else begin
    EditPW.PasswordChar := '*';
  end;
end;

【再掲:マウスカーソルの現在位置座標の取得方法】

座標チェックに☑すると、マウスカーソルの現在位置のスクリーン座標がリアルタイムで表示される。その方法は前回も示しましたが、次の通り。

マウスのカーソルが現在置かれている位置のスクリーン座標を取得してLabelに表示。

procedure TForm1.chkZahyoClick(Sender: TObject);
begin
  if chkZahyo.Checked then
  begin
    //Enabled
    Timer1.Enabled:=True;
  end else begin
    //Enabled
    Timer1.Enabled:=False;
    LabelXY.Caption:='[X座標, Y座標]';
  end;
end;

Timer1のOnTimerプロパティをダブルクリックして作成されたTimer1Timer手続きに次のコードを記述。これでほぼリアルタイムにカーソルの位置座標を取得して表示できる。

procedure TForm1.Timer1Timer(Sender: TObject);
var
  lh_Handle:  HWND;
  lpt_Pos:    TPoint;
  lrc_Rect:   TRect;
  lrg_Region: HRGN;
  li_Ret:     Integer;
begin
  if chkZahyo.Checked then
  begin
    //マウスカーソル位置をスクリーン座標で取得
    GetCursorPos(lpt_Pos);
    //自身のウィンドウリージョンを調べる
    lh_Handle := Self.Handle;

    //ウィンドウリージョン取得のため空のリージョンを作っておく
    lrg_Region := CreateRectRgn(0,0,0,0);
    try
      //ウィンドウリージョン取得
      li_Ret := GetWindowRgn(lh_Handle, lrg_Region);
      if (li_Ret <> ERROR) then begin
        //ウィンドウのRectを取得
        GetWindowRect(lh_Handle, lrc_Rect);
        //スクリーン座標からウィンドウの左上を原点とした座標に変換
        lpt_Pos.X := lpt_Pos.X - lrc_Rect.Left;
        lpt_Pos.Y := lpt_Pos.Y - lrc_Rect.Top;
        //ウィンドウリージョン内にマウスカーソルがあるかテスト
        if (PtInRegion(lrg_Region, lpt_Pos.X, lpt_Pos.Y)) then begin
          LabelXY.Caption:=Format('OK %d (%d-%d)', [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end else begin
          LabelXY.Caption:=Format('NG %d (%d-%d)', [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end;
      end else begin
        LabelXY.Caption:=Format('[X:%d, Y:%d]', [lpt_Pos.X, lpt_Pos.Y]);
      end;
    finally
      DeleteObject(lrg_Region);
    end;
  end;
end;

これでパスワード入力欄のスクリーン座標を取得・保存しておいて、実際はパスワード入力画面が表示されたら、「入力」ボタンをクリック(パスワード入力画面が表示されなければ、GUIそのものを表示する必要もないので、接続環境に合わせてGUIそのものの表示もON/OFFできるようにした)。もちろん、GUIの表示状態そのものを保存可能。

パスワードはクリップボードに送信せず、Editコントロールにマスクをかけて表示しておき、入力が必要であればボタンクリックで実行できるように設定。

入力ボタンをクリックすると指定座標位置へパスワードを送信

パスワードの半自動入力は、次のコードで実行(前回のID入力用コードを修正)。

procedure TForm1.btnCopyClick(Sender: TObject);
var
  dwFlags : DWORD;
  X,Y : Integer;
  LKeyByte : Byte;
begin

  boolInput:=False;

  //Information
  if chkInfo.Checked then
  begin
    if MessageDlg('パスワード入力画面が見えていて、入力欄は空欄ですか?', mtInformation, [mbYes, mbNo], 0) = mrYes then
    begin

      try

        //クリップボードを初期化
        Clipboard.Clear;
        //文字列をクリップボードへ
        Clipboard.AsText:=EditPW.Text;

        dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
        X:=Trunc(StrToInt(EditPWX.Text)/Screen.Width*65537);
        Y:=Trunc(StrToInt(EditPWY.Text)/Screen.Height*65535);

        //移動
        Mouse_Event(dwFlags,X,Y,0,0);
        //クリック
        Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
        //Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
        Application.ProcessMessages;

        WaitTime(StrToInt(cmbWaitTime.Text));

        Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
        Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
        Application.ProcessMessages;

        // [Ctrl] + [V] のキー操作
        LKeyByte := Ord('V');
        keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
        keybd_event(LKeyByte, MapVirtualKey(LKeyByte, 0), 0, 0);
        keybd_event(LKeyByte, MapVirtualKey(LKeyByte, 0), KEYEVENTF_KEYUP, 0);
        keybd_event(VK_CONTROL,MapVirtualKey(VK_CONTROL,0),KEYEVENTF_KEYUP, 0);

        boolInput:=True;


      except

        boolInput:=False;

      end;

      end else begin

        MessageDlg('パスワード入力画面を表示し、入力欄が空欄の状態で、再度実行してください。', mtInformation, [mbOk] , 0);

    end;

  end else begin

    try

      //クリップボードを初期化
      Clipboard.Clear;
      //文字列をクリップボードへ
      Clipboard.AsText:=EditPW.Text;

      dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
      X:=Trunc(StrToInt(EditPWX.Text)/Screen.Width*65537);
      Y:=Trunc(StrToInt(EditPWY.Text)/Screen.Height*65535);

      //移動
      Mouse_Event(dwFlags,X,Y,0,0);
      //クリック
      Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
      //Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
      Application.ProcessMessages;

      WaitTime(StrToInt(cmbWaitTime.Text));

      Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
      Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
      Application.ProcessMessages;

      // [Ctrl] + [V] のキー操作
      LKeyByte := Ord('V');
      keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
      keybd_event(LKeyByte, MapVirtualKey(LKeyByte, 0), 0, 0);
      keybd_event(LKeyByte, MapVirtualKey(LKeyByte, 0), KEYEVENTF_KEYUP,0);
      keybd_event(VK_CONTROL,MapVirtualKey(VK_CONTROL,0),KEYEVENTF_KEYUP,0);

      boolInput:=True;

    except

      boolInput:=False;

    end;
  end;
end;

5.画面の表示設定

IDのみの入力でOneDriveにサインイン可能な場合は、不要なGUIは表示せずに運用。

これが理想的画面

ID&パスワードの入力が必要な場合は、画面左上の設定ボタンをクリックしてGUIを表示。初回のみ、ID&パスワード入力欄のスクリーン座標を計測&保存して、次回以降は半自動入力でサインイン。

対症療法的&非理想的画面(GUIを活用してサインイン)

現実世界に追従するカタチでのプログラミングは、夢を追いかけて・・・ではなくて、正直、必要に追われて・・・って感じで、書いていて楽しくはないけど。

でも、もし、これが誰かの役に立つなら・・・

OneDriveが使えなくて、すごく困っている人の役に立つなら・・・

僕のしたことに、ほんの少しだけ

意味や価値を見出せる気がします。

そうだ・・・。今、思い出せた・・・。

プロが書いた、見た目も美しい、あらゆる要求に対応した高価なプログラムではなく、
こんな僕の書いた、みすぼらしい、しかも機能限定のプログラムがいいと・・・

二者択一の場面で、僕のプログラムを選んでくださる人がいることを。

うん。そうだ。
きみも言ってくれたね。

「生きていれば必ず前進できます。
 もっとよくなれるんです。

 ・・・

 お互い 夢の実現に向けて、自分らしく歩きましょう。」

今、信じなくて、いつ、信じるんだ。
1ミリでもかまわない。
前へ行くんだ。

僕に今できる、唯一、確かなことをするんだ・・・

6.まとめ

(1)OneDriveへのサインインはIDによりその挙動が異なる。
(2)対症療法的にプログラミングすれば半自動ログインはできる。
(3)全自動ログインには、Cookieの利用を含めたさらなる学習が必要。

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Lubricate the clutch wire

クラッチワイヤーのメンテナンス

バイクのクラッチレバーの付け根部分のグリスの汚れがひどくなってきた。
今日は時間もあることだし、クラッチレバー周辺をきれいに掃除しようか・・・と思った時、もう長いことクラッチワイヤーに注油してなかったことを思い出した。

清掃と注油。どうせならいっぺんにやってしまった方がいい。

そう思ってネットで検索すると専用の工具(ワイヤーインジェクター)を使わずに、でもムダなくスマートに注油する方法を発見。さっそく、やってみた。

買ってきたのはコレ!

で、専用工具(ワイヤーインジェクター)の代わりに用意したモノは、コレ!
(こっちは仕事でよく使うので、家に買い置きがたくさんあった)

チャック付きで、密封できることがポイントだそうです。(やってみて思いました。確かに!)
袋の大きさは幅50mm、高さ70mm、厚み0.08mmで強度が高いタイプ。
材質はポリプロピレン(PP)。

で、ケーブルのタイコ部分がギリ通過するよう、ポリ袋の右下をカットします(赤い点線部分)。失敗したくなかったので、最初は小さめに切って、取り外したケーブルのタイコ部分の大きさに合うよう、後から少しずつ穴を大きくしました。

赤の点線部分を小さめにカット

以下、ゼファー400での作業例です。
(他のバイクの場合、クラッチワイヤーをレバーから取り外す手順が異なります)

クラッチワイヤーの先端部分のタイコをクラッチレバーから外すため、クラッチレバーを前側に押して、アジャスタを「5」にセットします(5◀の状態)。こうすると、クラッチレバーが後ろに下がって、ケーブルのタイコ部分が外せる状態になるようです。

アジャスタを「5」にセット。

アジャスタの現在の状態を記録しておきます。ケーブルの交換ではないので、後からこの数値にセットすれば外す前の(=現在の)状態に戻るはずです。半クラッチの感覚(=レバーの握り代)は、アタマというより左手が覚えていますから、この数値は重要です。

現在の状態は「6mm」

ロックナットを弛めて、アジャスタを右へねじ込んで(写真のような状態)、クラッチレバーとロックナットとアジャスタの切り欠き(溝)とが一直線になるようにして、ケーブルを(写真の状態で向かって左へ)引っ張ると、アジャスタからアウターワイヤーのキャップ部分がうまいこと抜けてくれました(これで抜けない場合は、エンジン右側上のロックナットを弛めるんだそうです)。

アジャスタの回転が渋いのは、汚れたグリスに混じった砂?を噛んでいるためでした。
パーツクリーナーとナイロンブラシで清掃したらクルクルよく回るようになりました。

ワイヤーを外し、清掃した段階で、ケーブルの状態をチェックしました。サビやケバ立ち等はなく、30年前に買った時のままの純正ケーブルですが・・・ まだまだ大丈夫のようです。

ポリ袋にあけた穴にワイヤーケーブルのタイコ部分を通して、アウターワイヤーのキャップ部分がちょっと袋に入ったところで、穴の周囲にガムテープを巻き、ポリ袋とアウターワイヤーのキャップ部分を一体化させます。後でポリ袋に入れる潤滑油がすべてケーブル内へ流れ込むように、アウターワイヤーのキャップ部分をポリ袋内へ深く挿入しすぎないことがポイントです。

キャップ部分は浅く(少しだけ)ポリ袋に入れます。

ガムテープは、しっかりきつめに、2重巻きしました。

ここから潤滑油が漏れないことを祈ります・・・

流れ出た潤滑油を受ける、使い古した布をエンジンの上に用意します。

ポリ袋に潤滑油を注入します(見た感じで2~3ccくらい?)。注入後はポリ袋のチャックをしっかり閉じます。購入してきた潤滑油は発泡性で、ポリ袋がどんどん膨らみ、このガスの圧力で注油がスムーズに行われました。

潤滑油がすべてケーブル内に入るよう、ポリ袋の傾きを調整しながら作業しました。

途中、ガスの圧力が高まり、ポリ袋がパンパンになってチャックが開いてしまうかと思われたので、片手でチャック部分を押さえ、密閉状態を保ちました。

この後、ポリ袋はさらに膨らんだのでチャック部分全体を押さえました。

およそ1分後、注入した潤滑油が反対側から出てきました。

潤滑油は、初めから茶色だったので、これはアウターケーブル内の汚れではありません。

またとないチャンスですから、クラッチレバーと、その周辺もキレイに清掃しました。パーツクリーナーを吹いて、古いグリスを洗浄液で洗い流し、クラッチワイヤーの通り道をきれいにしました。結果、バイクは確かにキレイになりましたが、古いグリス混じりの洗浄液の跳ね返りが大量に飛び散って、服のあちこちに黒いシミが・・・。

早く脱いで、洗剤に漬けて、なんとか処置しないと・・・
彼女に叱られる・・・

写真に撮るとあちこち塗装が剥げていて、あらためて古いバイクだなーと思います。
このバイクを買ったとき、僕はまだ22歳だった・・・。

新しいグリスを塗って、クラッチワイヤーを元通りにセットします。

30分程度で作業は完了しました。

作業後、クラッチは思ったほど軽くはならず、むしろ、操作感の違い・・・今までのなんとなく「モッサリ」した感じから、元気溌剌とした感じに・・・ の方が印象に残りました。

服は汚れたけど、おもしろかったー☆

まとめ

(1)クラッチワイヤーへの注油は専用工具がなくても行える。
(2)ポリ袋は厚めを使用、発泡性の潤滑油を注入すると効率よく作業できる。
(3)パーツクリーナーの洗浄液の跳ね返りを浴びてもよい服装で作業する。

お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

My z33 with 20 inch wheels

My Z33 Version ST

ガレージで眠ってた深リムの20インチホイールを、この夏、久しぶりに履いた。
丸2年履いてた純正の18インチ(後期型)に噛ましてたワイトレ・・・
言いたくない理由で壊しちゃったから、なんだけど・・・。

走れない道や、入れない店があっても、20インチは別格だ。
フロント235/30、リア255/30だから、スペーサーなしでツライチに決まる。
車高短なんて、もう死語なんだろうけれど、笑いたいヤツは笑え。
やっぱり、コレがいい。
もし、今、苦手なものがあるとすれば、それは、路面のキャッツアイだけだ。

ずっと気になっていたフロント側を、今までより1cm下げた。

車輪止めを噛まし、覚悟をきめてジャッキアップ。
タイヤを外し、ホイールハウスに潜り込んで(今、地震 来るなよって祈りながら)、
車高調のネジ部分をナイロンブラシでキレイに掃除して・・・
固く締まっているロアブラケットのロックシートを舐めないように弛めて、
車高を下げたい分だけ、そうミリ単位で、このロックシートを上げておく。
次に、その上側のスプリングシートを時計回りに回転させて、
一緒にまわるショック本体を、ロアブラケット内に落とし込んでいく・・・。

この作業をする度に、必ず、思う。

オレはバカなんじゃないか・・・

自己満足のための、わずか1cm。

センターコンソールには、スクリーンが2つ、縦に並ぶ。
ここから見える風景が、たまらなく好きだ。

ここから先は・・・ もし、よかったら、
リンク先の音源、聴きながら読んでもらえないかな?

うん。今は、もう、古いクルマさ・・・
欧米なら、NISSAN 350Z って呼ぶのかな・・・。
日本では、FAIRLADY.
そう、フェアレディ Z33.

カタログで初めて見た・・・、あの日から
恋焦がれた、コックピット。

バケットシートに身を沈めて・・・
ステアリングを握る時は、いつも瞳を閉じてしまう。

・・・

FAIRLADY・・・
君が見える気がするんだ・・・。

バイクを眺めてるシーンを別にすれば・・・
こんなに、ドキドキする、空間はここしかない。

そうさ・・・ 僕のいちばん、好きな場所。

出会った、あの日から、変わらない
バケットシートの香り・・・

My FAIRLADY

z33。

きみが機械だなんて、僕には思えない・・・

RZ34は見たこともないし、z34には興味がない。

z33が好きだ。

死ぬまで、大好きだ。

燃費も最高さ☆
3.5Lのエンジン、気持ちよく回して、これだけ走れば上等でしょう?

6MTだけど・・・
気持ちよく走れるのは、3速まで。

一般道で、VQ35DE エンジンのトルクを楽しむには、3速が限界じゃないかな・・・。

でも・・・、こんなに凄いエンジンが市販されてる、この国が・・・好きだ。
この国に生まれて、ほんとうに、よかった・・・。

グラマラスなリアビュー。マフラーは柿本改。

トランク部分の造形は、『美しい』の一言に尽きて。
このかたちに決まるまで、どれほどの葛藤があったことだろう・・・。

最高のデザインじゃないか。

きみの名は、フェアレディ

美しい お嬢さん

なんて、いい、響きなんだろう。

僕の・・・最後の1台。

My FAIRLADY

僕の z33。

約束だよ・・・

お互いの命、ある限り・・・

きみと、いつも。

そう、 いつまでも・・・

走ろう!

Flight Simulator

出会い

かつてIBM製のPCを使っていたことがあった。ThinkPad220ってマシンだ。DOS/Vなんて今はもう聞かなくなった言葉が雑誌の表紙を飾っていた頃、僕は一冊の本に出会った。

ちょっと破れたけど、その本は帯付きでまだ僕の本棚に・・・

インターネット黎明期で、高速な光回線なんて存在しなかったし、それどころかADSLだっておとぎの国の夢物語のように感じてた僕だ。もちろん、Amazonもまだなかった。

だから、たぶん・・・八重洲あたりで、この本に出会ったんじゃないかな?
プログラミング関連の書籍(主なターゲットは、はじめC言語とVB・後にDelphi)は、地方の書店には、ほとんどなかったから、新幹線を利用する機会がある度に、この3月末になくなった・・・あの巨大な「ブックセンター」のPC関連の書籍売り場へ、僕は必ず足を運んでたんだ。

この本「徹底活用ブック」と銘打つだけあって、ThinkPad220に関するありとあらゆる情報が掲載されている感があり、どこから読んでも面白い本だった。新幹線の車内で電源を確保する方法など「こんなことやって、ほんとに大丈夫なのか?」と思っちゃったりもしたけど、そのゲーム機としての利用案内で知ったのが「Microsoft Flight Simulator Version 4」

(こんなん、あるんだー!)

それが「Flight Simulator」なるモノと、僕の出会いだった。

FS2020

FS98、FS2004どちらも楽しく遊べた。FS2004はWindowsXP時代のソフトで、インストールディスクなしで動かすには fs9.exe そのものを入れ替えるという裏技も必要だったりしたけど、ヤフオクで「Microsoft Force FeedBack2」なるジョイスティックも入手。現実世界では絶対に実現できない「火酒」を片手に操縦桿を握るという楽しみも、僕はこのFS2004で覚えた・・・。

FS2004で十分満足したためか、2006年に発表(日本語版の発売は2007年)されたFSXを、僕は購入しなかった。

そして今年。以前の職場で使っていたが、さまざまな理由から今の勤務先ではちょっと使えない自作PC(デスクトップ機)をどう使うか、思い悩んで。ふと思い立ったのがゲーム専用機として使えないか・・・ということ。FS2020がリリースされたのは知っており、そのあまりにもリアルな画面に心を奪われたことも本当なんだけど(太陽光が当たって出来る計器の影まで再現されたリリース当時のCM映像は衝撃そのものだった)、快適な動作環境として要求されるハードウェアのスペックが高すぎて、ゲームをするためだけに高価な機材を購入するのは到底無理と、あきらめていたのだった(当時、このデスクトップ機は職場で仕事に使用しており、自宅で使えるグラボ搭載可能なデスクトップ機を僕は持っていなかった)。

職場が変わって・・・、デスクトップ機は不要になり、今、それが自宅にある!

あらためて、FS2020を快適に動作させるために要求される理想スペックを調べてみた。
(知りたかったのは「推奨」ではなく、その上を行く「理想」スペック

OS:Windows10 64bit 2019年11月のアップデート適用済み
ストレージ:SSD 150GB
CPU:Intel Core i7-9800X
メモリ:32GB
グラボ:GeForce RTX2080

5年前に自作したPCだから、Windows11を入れるのはちょっと厳しい。でも、FS2020はWindows10でも動く。CPUはCore i7-7700だから理想スペックには、ちょっと足りないけど・・・搭載メモリを32GBにして、それなりの性能のグラフィックボードを載せれば、ある程度快適に飛べるんじゃないか?と思えてきた。幸いにして、最近はほとんど高額な買い物をしなかったので、貯金も復活。グラボの購入資金はある・・・。

早速、近所のPCデポに行ってメモリを購入。16GBのDDR4 SDRAM2枚セットで価格は¥17,500。お店の外に出て空を見上げたら、ちょうどジェット機が月をかすめて飛んでた。いい風景だった。

ホンモノが飛んでる・・・

グラボはAmazonで「ASUS NVIDIA GeForce RTX 2080 搭載 トリプルファンモデル 8GB ROG-STRIX-RTX2080-O8G-GAMING」の中古品を¥42,600で入手。

予想以上に箱が巨大でびっくり。梱包も豪華。値段だけのことはあります・・・。

GEFORCE RTX 2080載せてみました!

がらーんとしてた筐体の中が急に狭くなった感じ。グラボの存在感ってすごいんですね!
※ 電源ユニット交換後の写真

初めてこのグラボを持った時は、そのデカさと重さに驚き、果たしてMy デスクトップ機の筐体に収まるものか、不安になったけど。案ずるよりナントカで、そぉーっと挿入してネジ止めしたら、ずっと前から「わたし、ここにいました☆」みたいなイイ感じに。

CPUの冷却ファンに埃がたまっていたので、いい機会だと思い、外して清掃。ついでに5年間ご無沙汰状態のCPUも拝ませていただきましたが、5年前はペタペタしてた熱伝導グリスが乾き切って「粉」になっているのを発見。あわててAmazonで「ARCTIC MX-4( スパチュラ付き 4グラム) – サーマルコンパウンドペースト」を購入。税込み¥900なり。

商品の説明には「高い耐久性: 金属とシリコンの熱化合物とは対照的に、MX-4は時間の経過とともに損傷しません。 一度付けると、再度付着させる必要はありません(少なくとも8年間は持ちます)。」と書いてあった。

マザーボードはH270 Pro4で、256GBのM.2 SSDを載せてある。使わないソフトを全部削って、入ってるのはほとんどOSだけにしたら、空き容量は170GBになった。FS2020を入れるには150GB必要とのこと(アップデートするとこの半分程度になるという情報もWebにあった)。なら、これでギリ足りるはずだ。追加でダウンロードしなければならないデータが100GB程度あるようだけど、ほとんど使ってない容量4TBのHDDを載せてるから、FS2020起動時にちょっと時間がかかることさえ我慢できれば、なんとかなるはずだ。

そうそう、いちばん大切な主役を忘れていました。

Microsoft Flight Simulator : プレミアムデラックス 日本語版 ¥18,073

日本語版が入手できるなら、それに越したことはないか・・・と。

これで必要なモノは、全部揃ったはず。M.2 SSDの容量空けて、グラボも載せた。いよいよFS2020のインストールだ。で、電源をON。すると見たこともないメッセージが表示された・・・。なんじゃコレは・・・

「グラフィックカード用のPCle電源ケーブルを接続してください。」ってコト?

おかしいなー。ちゃんと電源ケーブル、グラボに差し込んだケド・・・。

不思議に思いながらGoogle先生にお伺いをたてると、なんと「グラボに供給される電力が足りてないから起動しない」ことが原因だと判明。

PLEASE POWER DOWN AND CONNECT THE PCle POWER CABLE(S) FOR GRAPHICS CARD 対処方法

https://favorite-fashion.com/blog110/amp/

そう言えば、購入したグラボには電源ケーブルの差し込みコネクタが2つあった。僕の手持ちの550W電源から供給できるグラボ用の電源ケーブルのコネクタは見た所1つしかない。グラボ側のコネクタのどっちに接続すれば、いいのか? ちょっと悩んで、あの時は、説明書にあった通り、左側のコネクタを選んだんだ。だから、右側のコネクタは空いてる・・・。

資金にまだ余裕はある・・・。慌ててバイクのキーを握り、ガレージに飛び込んで、エンジンに火を入れて、そのままPCデポへ直行。750Wの電源ユニットを購入。¥15,000

付けたばかりのグラボをいったん外して・・・

どこに、どんなカタチのコネクタが刺さっていたか、スマホで撮影しながら、電源ユニットを交換。マザーボードその他への電源供給コネクタを全て繋ぎ変えたことを慎重に確認してから、グラボを再度装着。グラボ行きの電源供給コネクタを探すと思った通り2つある。これで、グラボの電源ケーブル差し込みコネクタも無事全部埋まった。

650Wでもイケそうだったけど、敢えて750Wをチョイス。電気料金のことは遠い未来で考えることにする。

で、電源ON。今度は無事起動した。さっそくFS2020をインストール。覚悟していたけど、実際に半日かかった。これでも早い方らしい。ちなみにMy インターネット環境での回線速度は・・・

そんなに遅くはないと思うんだけど・・・

実は、ここまでは前置きで、ここからが本題

なんで今回僕がコレを書いたかというと、これから書くことをネットの片隅に記録しておきたかったから。

僕が調べた範囲では、この情報はどこにも書かれていなかった気がする。

それは何かというと、FS2020が自動認識&自動設定してくれない古いジョイスティックの接続設定方法。これを間違えたために、ホントに操縦苦労しました☆

ってか、キーボードならスイスイ飛ばせるのに、ジョイスティックで飛ぼうとすると滑走路から飛び立った瞬間に機首がグングン上を向いて、あっという間に失速して墜落。

これが現実なら、ほんとに命がいくつあっても足りません。
☆ちなみに僕は数十回、お亡くなりになりました☆

それでも慣れとは恐ろしいもので、FS2004では「あり得ない」くらい、ものすごく慎重に操縦桿を引いて、かろうじて離陸できるところまでウデを上げ・・・。ただ、離陸には成功しても、その後がさらにいけません。どう頑張っても水平飛行ができない。急上昇したり、急下降したり、常に機首を上下に振りながら飛んでる感じで、もう、どうにも、こうにも、ならなくて・・・。泣。

古いジョイスティックだからかなー☆

そう思って、使用を中止し、USBのプラグを引っこ抜いて、それからしばらくはキーボードで操縦していたのですが・・・、FS2020の解説本の記事から不具合の真の原因は、コントロール オプションで行う軸の割り当て設定の誤りであることが判明。

ジョイスティックの接続設定はコントロール オプションから行える。
ほんとにFS2020の画面はリアル・・・ってか、ホンモノは見たことないけど。

ここで、本来ならジョイスティックL軸Xを「エルロン軸」、ジョイスティックL軸Yを「エレベーター軸」に割り当てなければならないのに、僕は間違えて、ジョイスティックL軸Yを「エレベーターを下げる(ピッチを下げる)」と「エレベーターを上げる(ピッチを上げる)」に割り当ててしまっていたのだ。

これが正しい設定
赤枠部分を拡大

これでジョイスティックでスイスイ飛べるようになりました。
これまでさんざん苦労したのは、いったい何だったのか・・・

接続設定の誤りを教えてくれたのが、こちらの本。

著者である田中さん、ほんとうにありがとうです。

この本の18ページに、ジョイスティックの軸の割り当てについての解説があり、そこを読んでいて初めて割り当ての誤りに気づきました。この本に出会えて本当によかった!

ホントに飛んでるみたい・・・

来年は、要求スペックは現行のものと同じで、起動時間をさらに短縮したFS2024がリリースされるとのこと。

「Microsoft Flight Simulator 2024」の詳細を公開。四季の移り変わりや,嵐,オーロラの自然現象に加えて,路上の交通状況までも再現

https://www.4gamer.net/games/714/G071481/20230627020/

すごく、楽しみです!

お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

サインイン 2

追記(20230827 OneDriveアプリからオンライン表示へ切り替え)

無駄にプログラムなんか書く必要はありませんでした!

以下、『いかに苦労してOneDriveにサインインするか』という、上記サインイン 4に辿り着くまでの、長いながいまわり道の記録です。なので、お読みいただく価値がないことを最初に申し添えます。m(__)m

この記事は、アプリとして実行(タスクトレイに常駐)するOneDriveではなく、Web上のOneDriveへ直接データをアップロードし、別のPCでそのデータをダウンロードする、言わばデータ交換用USBメモリのようにOneDriveを使用する方法の一例です。PC内のOneDriveフォルダにあるデータと、クラウド上のOneDriveにあるデータの同期などは、まったく考慮しておりませんので、その点にはどうかご注意願います。

また、サインイン画面でのID(Microsoftアカウントに登録したメールアドレス)入力後のOneDriveの応答について、いろいろ調べたのですが、起こり得る個々の問題一つ一つについては、僕のプログラミングスキルでは到底対応できないと感じました。そこで、ID入力後にパスワード入力が必要になった場合の処理について、後日、自分なりのスーパー・ローレベル対応方法として「サインイン3」というタイトルで書きたいと思います。

コンピューターと同期する OneDrive フォルダーを選択する

クラウド上のOneDriveとPCのデータの同期については上のリンク先記事をご参照ください。

OneDriveのサインイン画面に、IDとして利用するメールアドレスを自動入力するプログラムを前回、作成した。目的通りのプログラムが出来たことは出来たが、WebView4Delphiコンポーネント(MITライセンス)のdemoフォルダにあったSampleをそのまま使わせてもらったため、「ID(メールアドレス)を自動入力する」以外にも様々な機能が実装(Sampleなんだから当然と言えば当然)されており、ただプログラムを起動するだけで、exeを置いたフォルダ内に容量がおよそ200MBくらいある「CustomCache」という名前のフォルダが出来てしまう。無視すればイイと言ってしまえば・・・それまでかもしれないけど・・・。

WebView4Delphi

https://github.com/salvadordf/WebView4Delphi

とりあえず、自分的には使わない機能をカットしようと思い、プログラムソースを読んでみたんだけれど・・・、コードどうしの関連がよくわからない。ヘタにいじって不具合とバグの山を築くより、「IDの自動入力」という初心に帰って、プログラムをイチから作り直した方がいい気がしてきた。

で、作ったのがコレ。

OneDriveのサインイン画面にID(メールアドレス)を自動入力する機能だけを搭載

【作成の手順】

1.TEdgeBrowserを使う
2.GUIの設計とプログラムコード
(1)GUIの設計
(2)VCLコントロールの表示/非表示を切り替え
(3)入力値の保存/読み込みと暗号化
(4)カーソル位置の座標を取得
(5)プログラムコードから指定位置をクリック
(6)ダウンロードフォルダを開く
(7)リソースにDLLを埋め込む
(8)操作方法の案内
3.まとめ
4.お願いとお断り

1.TEdgeBrowserを使う

Delphiで、Web コンテンツやローカルに置いたhtmlファイルの読み込みと表示を行うためのビジュアル コンポーネントには、TEdgeBrowser や TWebBrowser があるけれど、表示したいWebページがJavaScript のダイアログ ボックス、パネル、その他要素を使用しているとTWebBrowser では Web ページを正しく表示できないことがあるようだ。

正直に言うと、Edge には印刷その他の不具合でかつて悩まされた記憶があり、個人的にあまり良いイメージを持っていなかったので、新しい TEdgeBrowser コンポーネントではなく、古い TWebBrowser コンポーネントの方を使いたかった。だから、最初は、次のリンク先にあるような情報を参考にして、TWebBrowser コンポーネントで OneDrive のサインイン画面を表示するプログラムを書いてみたのだが・・・

Delphi / C++Builder Starter Edition の VCL で WebBrowser コンポーネントを使う

https://qiita.com/ht_deko/items/c69902d644ea03f61deb

上のリンク先記事のおしまいの部分でも述べられている通り、TWebBrowser コンポーネントを使って OneDrive のサインイン画面を表示するコードを書くと、結構盛大にスクリプトエラー発生のメッセージが表示される。

なんとかならないか・・・と思い、Google先生にお伺いをたてると、FMX版の TWebBrowser の記事ではあるが、本家本元embarcaderoさん提供のスクリプトエラー発生回避策を発見。

FMX.WebBrowser.TWebBrowser

https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.WebBrowser.TWebBrowser

それによれば「この問題を回避するには、アプリケーションは、Internet Explorer の FEATURE_BROWSER_EMULATION 機能を使用して、Web ページを IE11 エッジ モードで表示しなければなりません。」と説明があり、具体的な回避策として「FormCreate イベント ハンドラで、TForm1.SetPermissions メソッドを呼び出す」方法がソースコード付きで掲載されていた。

早速、スクリプトエラー回避策なるそのコードをコピペして実行してみたが、ナニがよくないのか、スクリプトエラーは発生状況に変化は見られなかった。

上のリンク先ページでは、スクリプトエラー回避策コードの下に「メモ: レジストリに対するこれらの変更は、アプリケーションが開始する前に適宜行わなければなりません。最初にアプリケーションを開始した際には、それを一度閉じ、再度開始します。」という説明があるので、プログラム起動時にレジストリに対する変更を行って、いったんプログラムを終了し、再度実行すればOKなのか? とも思ったが、原因の究明に時間を割くより、新しいTEdgeBrowserコンポーネントでOneDriveのサインイン画面を表示する方法を試した方が賢い気がして、ここで方針を変更。素直にTEdgeBrowserコンポーネントを使うことにする。その際、参考にさせていただいた記事がこちら

TEdgeBrowserでWebView2を使う ~Delphiソースコード集

https://mam-mam.net/delphi/tedgebrowser.html

OSがWindows11であれば、動作に必要なMicrosoft WebView2 ランタイムは、既に入っているので、インストール不要とのこと。作成するプログラムを動かす予定のPCのOSはすべてWindows11なので、その点は心配ないが、いちおうReadme.txtファイルを用意して、OSがWindows10の場合にはMicrosoft WebView2 ランタイムのインストールが必要であることを案内した方がよさそうだ。

2.GUIの設計とプログラムコード

(1)GUIの設計

Delphiを起動し、「ファイル」→「新規作成」→「Windows VCLアプリケーション」と辿って、表示されたFormにPanelを3つ図のように配置する。

Panel1が階層構造的にはいちばん下にあり、AlignプロパティはalTopを指定。その上にPanel2及び 3 を乗せて、Panel2のAlignプロパティはalLeft、Panel3のAlignプロパティはalClientをそれぞれ指定する。このようにプロパティを設定しておけば、Formの大きさが変化しても、Panel2の大きさ(幅と高さ)は変わらず、Panel3の高さはそのままで幅がFormの大きさ(幅)に合わせて自動的にサイズが変化し、各VCLコントロールの位置はFormの左上を原点とした設計時の位置に固定されて表示される。

VCLコントロールの階層構造

この後、Panel2の上には「ダウンロードフォルダを開く」ボタン、Panel3の上には暗号化してiniファイルに保存する予定の「ID」入力用のEditその他のVCLコントロールを次のように配置する。

必要と思われる最小限度のVCLコントロールを使い勝手を考えながら配置する

(2)VCLコントロールの表示/非表示を切り替え

ID(メールアドレス)や、自動的にクリックする座標値はいったん設定・保存してしまえば、通常の使用の際には必要ないので、普段は非表示に設定。つまり、□設定チェックボックス以外のVisibleプロパティはすべてFalseを指定する。で、設定を☑したときだけ、表示されるように設定。

procedure TForm1.chkSettingClick(Sender: TObject);
begin
  if chkSetting.Checked then
  begin
    LabelID.Visible:=True;
    btnCopy.Visible:=True;
    btnCopy.Enabled:=True;
    Edit1.Visible:=True;
    LabelX.Visible:=True;
    EditX.Visible:=True;
    LabelY.Visible:=True;
    EditY.Visible:=True;
    btnSave.Visible:=True;
    chkZahyo.Visible:=True;
    LabelXY.Visible:=True;
    LabelWaitTime.Visible:=True;
    cmbWaitTime.Visible:=True;
  end else begin
    LabelID.Visible:=False;
    btnCopy.Visible:=False;
    Edit1.Visible:=False;
    LabelX.Visible:=False;
    EditX.Visible:=False;
    LabelY.Visible:=False;
    EditY.Visible:=False;
    btnSave.Visible:=False;
    chkZahyo.Visible:=False;
    LabelXY.Visible:=False;
    LabelWaitTime.Visible:=False;
    cmbWaitTime.Visible:=False;
  end;
end;

(3)入力値の保存/読み込みと暗号化

各VCLコントロールに入力された値は、必要な個所は暗号化してiniファイルに保存する。

uses
  System.IniFiles;

procedure TForm1.btnSaveClick(Sender: TObject);
var
  strID:string;
  Ini:TIniFile;
begin

  //入力の有無をCheck
  if Edit1.Text='' then
  begin
    MessageDlg('IDとして利用するメールアドレスを入力してください', mtInformation, [mbOk] , 0);
    Edit1.SetFocus;
    Exit;
  end;

  if (EditX.Text='') or (EditY.Text='') then
  begin
    if EditX.Text='' then
    begin
      MessageDlg('自動クリックするX座標を入力してください', mtInformation, [mbOk] , 0);
      EditX.SetFocus;
    end;
    if EditY.Text='' then
    begin
      MessageDlg('自動クリックするY座標を入力してください', mtInformation, [mbOk] , 0);
      EditY.SetFocus;
    end;
    Exit;
  end;

  if cmbWaitTime.Text='' then
  begin
    MessageDlg('カーソル移動の待機時間をミリ秒単位で入力してください', mtInformation, [mbOk] , 0);
    cmbWaitTime.SetFocus;
    Exit;
  end;

  //暗号化
  strID:=EDText(Edit1.Text, IntToStr(HashOf('XXXXXXXX')), True);

  //iniファイルに保存
  Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //保存
    Ini.WriteString('Section', 'ID', strID);
    Ini.WriteString('Section', 'IchiX', EditX.Text);
    Ini.WriteString('Section', 'IchiY', EditY.Text);
    Ini.WriteString('Section', 'WaitTime', cmbWaitTime.Text);
    //Userに通知
    MessageDlg('現在の設定を保存しました!', mtInformation, [mbOk] , 0);

    if not btnCopy.Enabled then btnCopy.Enabled:=True;

  finally
    Ini.Free;
  end;

end;

コードの中で使用しているEDText関数はテキスト暗号化の関数。

  private
    { Private 宣言 }
    //HashNameMBCS(Create hashed values from a Unicode string)
    //MBCS:Multibyte Character Set=マルチバイト文字セット
    function HashOf(const key: string): cardinal;

    //テキスト暗号化/復号化
    Function EDText(KeyStr,PassW:string; EncOrDec:Boolean):string;
    //KeyStr:平文 or 暗号化文のいずれかを指定
    //PassW:パスワード
    //EncOrDec:True -> Encode / False -> Decode

  public
    { Public 宣言 }
  end;

function TForm1.HashOf(const key: string): cardinal;
var
  I: integer;
begin
  Result := 0;
  for I := 1 to length(key) do
  begin
    Result := (Result shl 5) or (Result shr 27);
    Result := Result xor Cardinal(key[I]);
  end;
end;

function TForm1.EDText(KeyStr, PassW: string; EncOrDec: Boolean): string;
var
  {暗号化用変数}
  Source, Dest, Password:TStringBuilder;
  lpSource, lpPass:Integer;
  PassValue, SourceValue, EDValue:Word;
  {共用変数}
  //乱数の種
  Seed1,Seed2,Seed3:integer;
  //実数の一様乱数
  RandNum:Double;
  //秘密鍵Seed
  Seed:string;
  {復号化用変数}
  DecSource:string;
begin
  //1.シード値を準備
  // (1)Passwordを整数へ変換→シード値1へ代入
  Password := TStringBuilder.Create;
  //Seed1を初期化
  //Seed1:=0;
  try
    Password.Append(PassW);
    PassValue := 0;
    for lpPass := 0 to Password.Length - 1 do
    begin
      //パスワード→整数
      PassValue := PassValue + Word(Password.Chars[lpPass]);
    end;
    Seed1:=PassValue;
  finally
    Password.Free;
  end;

  // (2)パスワード文字列の長さを取得→シード値2へ代入
  Seed2:= ElementToCharLen(PassW,Length(PassW));

  // (3)シード値1とシード値2の排他的論理和を計算して、シード値3へ代入
  Seed3 := Seed1 xor Seed2;

  //2.実数の一様乱数を計算
  //---------------------------------------------------------------------------
  // 0より大きく1より小さい実数の一様乱数を発生する関数
  // B.A.Wichmann and I.D.Hill, Applied Statistics, 31, 1982, p.188 に基づく
  // Seed1-3に入れる初期値(整数)は16bit長(maxint=32767)で十分
  // Seed1-3には1から30000までの任意の整数値を準備する(0ではいけない)
  //---------------------------------------------------------------------------

  //Seed1:=171*Seed1 mod 30269 と同値
  Seed1:=(Seed1 mod 177)*171-(Seed1 div 177)* 2;
  if Seed1<0 then Seed1:=Seed1+30269;
  //Seed2:=172*Seed1 mod 30307 と同値
  Seed2:=(Seed2 mod 176)*172-(Seed2 div 176)* 35;
  if Seed2<0 then Seed2:=Seed2+30307;
  //Seed1:=170*Seed1 mod 30323 と同値
  Seed3:=(Seed3 mod 178)*170-(Seed3 div 178)* 63;
  if Seed3<0 then Seed3:=Seed3+30323;
  //See1-3それぞれの乱数を0<RandNum<1となるように
  //計算結果が0より大きく、1未満の実数に直し、和の小数部分をとる
  RandNum:=(Seed1/30269.0) + (Seed2/30307.0) + (Seed3/30323.0);
  while RandNum>=1 do RandNum:=RandNum-1;

  //3.秘密鍵を生成

  //整数の一様乱数の上限値を決めて、整数の一様乱数を生成し、
  //これに上で計算した実数の一様乱数を加えて秘密鍵を生成する
  //Seedが秘密鍵(文字列として利用)となる
  Seed:= FloatToStr(RandNum + trunc((Seed1+Seed2+Seed3)*RandNum));

  //4.暗号化 / 復号化
  if (EncOrDec) then
  begin
    //暗号化(Encode)
    Source := TStringBuilder.Create;
    Dest := TStringBuilder.Create;
    Password := TStringBuilder.Create;
    try
      Source.Append(KeyStr);
      //秘密鍵をセット
      Password.Append(Seed);
      lpPass := 0;
      //テキストのエンコード
      for lpSource := 0 to Source.Length - 1 do
      begin
        //パスワード→整数
        if Password.Length = 0 then
          PassValue := 0
        else begin
          PassValue := Word(Password.Chars[lpPass]);
          Inc(lpPass);
          if lpPass >= Password.Length then lpPass := 0;
        end;
        //テキスト→整数
        SourceValue := Word(Source.Chars[lpSource]);
        //XOR演算
        EDValue := PassValue xor SourceValue;
        //16進数文字列に変換
        Dest.Append(IntToHex(EDValue, 4));
        //処理結果を返り値にセット
        Result:=Dest.ToString;
      end;
    finally
      Password.Free;
      Dest.Free;
      Source.Free;
    end;
  end else begin
    //復号化(Decode)
    DecSource:=keyStr;
    Dest := TStringBuilder.Create;
    Password := TStringBuilder.Create;
    try
      //暗号化テキストのデコード
      Dest.Clear;
      Password.Clear;
      //秘密鍵をセット
      Password.Append(Seed);
      lpPass := 0;
      for lpSource := 1 to Length(DecSource) div 4 do
      begin
        SourceValue := StrToInt('$' + Copy(DecSource, (lpSource - 1) * 4 + 1, 4));
        if Password.Length = 0 then
          PassValue := 0
        else
        begin
          PassValue := Word(Password.Chars[lpPass]);
          Inc(lpPass);
          if lpPass >= Password.Length then lpPass := 0;
        end;
        EDValue := SourceValue xor PassValue;
        Dest.Append(Char(EDValue));
      end;
      //処理結果を返り値にセット
      Result:=Dest.ToString;
    finally
      Password.Free;
      Dest.Free;
    end;
  end;
end;

サインイン時にIDとして入力するメールアドレスは暗号化されてiniファイルに保存され、FormCreate時にこれを復号して、Editコントロールに表示する。

procedure TForm1.FormCreate(Sender: TObject);
var
  Ini: TIniFile;
  strID, strX, strY, strWaitTime: String;
  i:integer;
begin

  //Formを最大化して表示
  Form1.WindowState:=wsMaximized;

  //待ち時間の選択肢(100~3000ミリ秒を100ミリ秒単位で用意)
  for i := 1 to 30 do
  begin
    cmbWaitTime.Items.Add(IntToStr(i*100));
  end;

  //iniファイルの存在を確認
  if FileExists(ChangeFileExt(Application.ExeName, '.ini')) then
  begin
    //iniファイルからデータを読込み
    Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
    try
      strID:=Ini.ReadString('Section', 'ID', '');
      strX:=Ini.ReadString('Section', 'IchiX', '580');
      strY:=Ini.ReadString('Section', 'IchiY', '420');
      strWaitTime:=Ini.ReadString('Section', 'WaitTime', '500');
    finally
      Ini.Free;
    end;
    //復号して表示
    Edit1.Text:=EDText(strID, IntToStr(HashOf('XXXXXXXX')), False);
    EditX.Text:=strX;
    EditY.Text:=strY;
    cmbWaitTime.Text:=strWaitTime;
  end;

  //Navigate
  EdgeBrowser1.Navigate('https://onedrive.live.com/about/ja-jp/signin/');

end;

(4)カーソル位置の座標を取得

マウスのカーソルが現在置かれている位置のスクリーン座標を取得してLabelに表示。

procedure TForm1.chkZahyoClick(Sender: TObject);
begin
  if chkZahyo.Checked then
  begin
    //Enabled
    Timer1.Enabled:=True;
  end else begin
    //Enabled
    Timer1.Enabled:=False;
    LabelXY.Caption:='[X座標, Y座標]';
  end;
end;

Timer1のOnTimerプロパティをダブルクリックして作成されたTimer1Timer手続きに次のコードを記述。これでほぼリアルタイムにカーソルの位置座標を取得して表示できる。

procedure TForm1.Timer1Timer(Sender: TObject);
var
  lh_Handle:  HWND;
  lpt_Pos:    TPoint;
  lrc_Rect:   TRect;
  lrg_Region: HRGN;
  li_Ret:     Integer;
begin
  if chkZahyo.Checked then
  begin
    //マウスカーソル位置をスクリーン座標で取得
    GetCursorPos(lpt_Pos);
    //自身のウィンドウリージョンを調べる
    lh_Handle := Self.Handle;

    //ウィンドウリージョン取得のため空のリージョンを作っておく
    lrg_Region := CreateRectRgn(0,0,0,0);
    try
      //ウィンドウリージョン取得
      li_Ret := GetWindowRgn(lh_Handle, lrg_Region);
      if (li_Ret <> ERROR) then begin
        //ウィンドウのRectを取得
        GetWindowRect(lh_Handle, lrc_Rect);
        //スクリーン座標からウィンドウの左上を原点とした座標に変換
        lpt_Pos.X := lpt_Pos.X - lrc_Rect.Left;
        lpt_Pos.Y := lpt_Pos.Y - lrc_Rect.Top;
        //ウィンドウリージョン内にマウスカーソルがあるかテスト
        if (PtInRegion(lrg_Region, lpt_Pos.X, lpt_Pos.Y)) then begin
          LabelXY.Caption:=Format('OK %d (%d-%d)', [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end else begin
          LabelXY.Caption:=Format('NG %d (%d-%d)', [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end;
      end else begin
        LabelXY.Caption:=Format('[X:%d, Y:%d]', [lpt_Pos.X, lpt_Pos.Y]);
      end;
    finally
      DeleteObject(lrg_Region);
    end;
  end;
end;

(5)プログラムコードから指定位置をクリック

前回作成したプログラムでいちばん、悩んだのがここ。最初はサインイン画面のウィンドウハンドルを取得して文字列を送信しようと思ったんだけれど・・・これがうまくいかない。その詳細は前回の記事を参照してください。

さんざん悩んで、ようやく思いついた方法がプログラムコードで画面上の任意の位置をクリックする方法。Formが完全に描画された段階で、指定位置のクリックと、その位置への文字列の入力を実行している。そのコードを再掲。

  private
    { Private 宣言 }

    //アドレス貼り付け実行の成否
    boolInput:boolean;

    fgWaitBreak : boolean;  //変数は「functionより先に定義」する

    //待ち関数  指定カウントが経過すれば True, 中断されたならば False
    function WaitTime(const t: integer): Boolean;

    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;


//待機関数
function TForm1.WaitTime(const t: integer): Boolean;
var
  Timeout: TDateTime;
begin
  //待ち関数  指定カウントが経過すれば True, 中断されたならば False
  fgWaitBreak := False;
  Timeout := Now + t/24/3600/1000;
  while (Now < Timeout)and not fgWaitBreak do begin
    Application.ProcessMessages;
    Sleep(1);
  end;
  Result := not fgWaitBreak;
end;


procedure TForm1.CMShowingChanged(var Msg: TMessage);
var
  dwFlags : DWORD;
  X,Y : Integer;
  LKeyByte : Byte;
begin
  inherited; {通常の CMShowingChagenedをまず実行}
  if Visible then
  begin

    Update; {完全に描画}

    if Edit1.Text='' then
    begin
      Edit1.SetFocus;
      Exit;
    end;

    if (EditX.Text='') or (EditY.Text='') then
    begin
      if EditX.Text='' then EditX.SetFocus;
      if EditY.Text='' then EditY.SetFocus;
      Exit;
    end;

    fgWaitBreak:=False;

    //さらに念のためちょっと待機
    WaitTime(StrToInt(cmbWaitTime.Text));

    dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;

    //クリック位置を取得
    X:=Trunc(StrToInt(EditX.Text)/Screen.Width*65537);
    Y:=Trunc(StrToInt(EditY.Text)/Screen.Height*65535);

    //移動
    Mouse_Event(dwFlags,X,Y,0,0);
    Application.ProcessMessages;

    //クリック
    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Application.ProcessMessages;

    WaitTime(StrToInt(cmbWaitTime.Text));

    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Application.ProcessMessages;
    Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);

    //文字列を送信
    boolInput:=False;
    try

      //クリップボードを初期化
      Clipboard.Clear;

      //文字列をクリップボードへ
      Clipboard.AsText:=Edit1.Text;
      
      //[Ctrl] + [V] のキー操作
      LKeyByte := Ord('V');
      keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
      keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   0, 0);
      keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   KEYEVENTF_KEYUP, 0);
      keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), KEYEVENTF_KEYUP, 0);
      //操作に成功
      boolInput:=True;
    except
      //操作に失敗
      boolInput:=False;
    end;

    //貼り付け操作に成功した場合は入力ボタンを操作不可に設定
    if boolInput then btnCopy.Enabled:=False;

  end;

end;

{入力ボタンのClick手続きは、確認メッセージの表示以外は上のコードとほとんど同じ}

また、予期せぬ事故を防止するため、プログラムの終了時にはクリップボードを空に(初期化)する。

procedure TForm1.FormDestroy(Sender: TObject);
begin
  //クリップボードを初期化
  Clipboard.Clear;
end;

(6)ダウンロードフォルダを開く

OneDriveからデータのダウンロードが無事終了すれば、次のようにダウンロードフォルダを開くリンク付きのWindowが表示されるから、特殊なフォルダである「ダウンロードフォルダを開く」ボタンは、別になくてもかまわない気もするけど。

このWindowは移動できない?

もしかしたら任意のタイミングで、それを開きたい時があるかもしれない。エクスプローラーを開けばいいじゃないかという意見は、ここでは聞かなかったことに。

ダウンロードフォルダを開くプログラムコード。

uses
  Vcl.Clipbrd, System.IniFiles, System.UITypes,
  Winapi.ShlObj, Winapi.KnownFolders, Winapi.ShellAPI;

procedure TForm1.btnOpenDLFolderClick(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
  D_FolderPath, ExeFileName:string;
  LhInst:Cardinal;
begin
  FolderID:=StringToGUID('{374DE290-123F-4565-9164-39C4925E467B}');
  if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
  begin
    D_FolderPath := FolderPath;
    //確認
    //ShowMessage(D_FolderPath);
    //ダウンロードフォルダを開く
    ExeFileName:= 'explorer.exe';
    LhInst:=ShellExecute(Handle, 'open', PChar(ExeFileName), PChar(D_FolderPath), nil, SW_SHOW);
    if LhInst <= 32 then
    begin
      MessageBox(Handle, '起動に失敗しました.', '情報', MB_ICONINFORMATION);
    end;
  end;
end;

(7)リソースにDLLを埋め込む

このプログラムの動作には「WebView2Loader.dll」が必須(WebView2Loader.dll は、アプリがデバイス上で WebView2 ランタイム (Microsoft Edge プレビュー チャネル) を見つけるのに役立つコンポーネントであるとのこと)。

WebView2 アプリを 1 つの実行可能ファイルとして配布する

https://learn.microsoft.com/ja-jp/microsoft-edge/webview2/how-to/static

このDLLがないと困るので、添付忘れを防止するため、リソースに埋め込んでおいて、プログラムの実行時にexeのある場所にその有無を確認し、なければリソースから生成するように設定。

メニューの「プロジェクト」→「リソースと画像」で埋め込むDLLを指定

で、FormCreate時に有無を確認、なければexeのある場所に生成。

procedure TForm1.FormCreate(Sender: TObject);
var
  Ini: TIniFile;
  strID, strX, strY, strWaitTime: String;
  i:integer;
  dllFileName:string;
begin

  //リソースからDLLを(なければ)生成
  //rijnファイルの位置を指定
  dllFileName:=ExtractFilePath(Application.ExeName)+'WebView2Loader.dll';
  //rijnファイルの存在を確認
  if not FileExists(dllFilename) then
  begin
    //リソースを再生
    with TResourceStream.Create(hInstance, 'Resource_1', RT_RCDATA) do
    begin
      try
        SaveToFile(dllFileName);
      finally
        Free;
      end;
    end;
  end;

  ・・・

end;

(8)操作方法の案内

この他に、画面最下部に設置したStatusBarに次のような案内を表示できるようにした。

操作方法の案内をStatusBarに表示(OKをクリックすると消える)
案内を表示する/しないはユーザーが選択して、その設定状態の保存も可能に

操作方法の案内の表示/非表示の切り替え。

procedure TForm1.chkInfoClick(Sender: TObject);
var
  strInfo:string;
  strWidth:integer;
begin
  if chkInfo.Checked then
  begin
    //表示する文字列
    strInfo:='ID(メールアドレス)が自動入力されないときは、Ctrl+V で入力できます!';
    strWidth:=StatusBar1.Canvas.TextWidth(strInfo);
    btnOK.Visible:=True;
    with btnOK do
    begin
      Parent:=StatusBar1;
      Left:=strWidth-20;
      Top:=1;
    end;
    //StatusBar1の設定(重要:このプロパティがFalseだとStatusBarにテキストが表示されない)
    StatusBar1.SimplePanel:=True;
    //Info
    StatusBar1.SimpleText:=strInfo;
  end else begin
    StatusBar1.SimpleText:='';
    btnOK.Visible:=False;
  end;
end;

案内を「表示する」が選ばれていた場合はFormCreate時に案内表示を出すよう設定。

procedure TForm1.FormCreate(Sender: TObject);
var
  Ini: TIniFile;
  strID, strX, strY, strWaitTime: String;
  i:integer;
  dllFileName:string;
  strWidth:Integer;
  strInfo:string;
  boolInfo:boolean;
begin

  if chkInfo.Checked then
  begin
    //表示する文字列
    strInfo:='ID(メールアドレス)が自動入力されないときは、Ctrl+V で入力できます!';
    strWidth:=StatusBar1.Canvas.TextWidth(strInfo);
    with btnOK do
    begin
      Parent:=StatusBar1;
      Left:=strWidth-20;
      Top:=1;
    end;
    //StatusBar1の設定(重要:このプロパティがFalseだとStatusBarにテキストが表示されない)
    StatusBar1.SimplePanel:=True;
    //Info
    StatusBar1.SimpleText:=strInfo;
  end;

  ・・・

  //iniファイルの存在を確認
  if FileExists(ChangeFileExt(Application.ExeName, '.ini')) then
  begin
    //iniファイルからデータを読込み
    Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
    try
      strID:=Ini.ReadString('Section', 'ID', '');
      strX:=Ini.ReadString('Section', 'IchiX', '580');
      strY:=Ini.ReadString('Section', 'IchiY', '420');
      strWaitTime:=Ini.ReadString('Section', 'WaitTime', '500');
      boolInfo:=Ini.ReadBool('Section','Info',True);
    finally
      Ini.Free;
    end;
    //復号して表示
    Edit1.Text:=EDText(strID, IntToStr(HashOf('adminy')), False);
    EditX.Text:=strX;
    EditY.Text:=strY;
    cmbWaitTime.Text:=strWaitTime;
    chkInfo.Checked:=boolInfo;
  end;

  ・・・

end;

案内そのものを表示したくない場合は、ユーザーの自由意思でその設定も可能に。

procedure TForm1.btnSaveClick(Sender: TObject);
var
  strID:string;
  Ini:TIniFile;
begin

  //入力の有無をCheck
  ・・・

  //暗号化
  strID:=EDText(Edit1.Text, IntToStr(HashOf('adminy')), True);

  //iniファイルに保存
  Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //保存
    Ini.WriteString('Section', 'ID', strID);
    Ini.WriteString('Section', 'IchiX', EditX.Text);
    Ini.WriteString('Section', 'IchiY', EditY.Text);
    Ini.WriteString('Section', 'WaitTime', cmbWaitTime.Text);
    Ini.WriteBool('Section','Info',chkInfo.Checked);
    //Userに通知
    MessageDlg('現在の設定を保存しました!', mtInformation, [mbOk] , 0);

    if not btnCopy.Enabled then btnCopy.Enabled:=True;

  finally
    Ini.Free;
  end;

end;

3.まとめ

(1)TEdgeBrowserを使えばOneDriveのサインイン画面をエラーなしで表示できる。
(2)サインイン画面へのIDの入力はプログラムコードで実行可能。
(3)IDはクリップボードに送信しておき、Ctrl+Vでも貼り付け可能に設定。

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

サインイン

追記(20230827 OneDriveアプリからオンライン表示へ切り替え)

無駄にプログラムなんか書く必要はありませんでした!

以下、『いかに苦労してOneDriveにサインインするか』という、上記サインイン 4に辿り着くまでの、長いながいまわり道の記録です。なので、お読みいただく価値がないことを最初に申し添えます。m(__)m

OneDrive接続専用Browserを作る!

OneDriveのサインイン画面にメールアドレスを自動入力
あとはEnterキーを叩くだけ
どうしてもコレを実現したかった!

【今回の記事】

1.動機は同期
2.WebView4Delphiコンポーネントを使う
3.プログラムでクリックを実行
4.特殊なフォルダを表示する
5.まとめ
6.お願いとお断り

1.動機は同期

この春から勤務先が変わり、それに伴って職場のPC環境も大きく変化して、ファイルのやり取りにOneDriveを利用することが多くなった。今までは、Windows11のアプリとして設定されているOneDriveを常に起動しておいて、必要な時、タスクバーから呼び出して使っていたが、バックアップ的な意味合いで利用することが多く、ファイル交換用途での利用はそれほど多くなかった。

複数のPC間でのデータのやりとりにOneDriveを利用するしかない現在の環境では、今まであまり考えたことがなかった同期のタイミングが問題になってきた。特に、それが『今すぐ』別のPCで使いたいファイルの場合、こっちのPCからクラウドにデータをアップロードして、直ちに、あっちのPCでそのデータをダウンロードしたいのだけれど、アプリのOneDriveでそれを実現する方法がわからない。

Google先生に尋ねても、『コレだ!』という答えは見つからず・・・。
(OneDriveの正しい使い方を私が知らないだけなのかもしれませんが)

仕方がないからアプリではなく、WebブラウザからOneDriveにサインインして、別のPCですぐに使いたいファイル(やフォルダ)をアップロード。別PC側でも同様にしてWebブラウザからOneDriveにサインイン、目的のファイル(やフォルダ)をダウンロードしていたんだけど・・・。

いったん接続してしまえば、ほっといても特に問題はないし、最新の状態に表示を更新したければ F5キー を押すだけだから、いいっちゃいいんだけど・・・。

OneDriveへのサインイン時にメールアドレスを入力するのが、かなりめんどくさい。

特に急いでいる時に入力を間違えたりすると、余計、イライラして、精神的に非常によろしくない。メモ帳にID替わりのメールアドレスを入力、デスクトップに保存しておいて、それをコピペすればいいかと思ったのだけれど、それすら面倒に感じてしまう自分がいよいよ情けなくなった・・・。

魂が腐っている・・・

他に作りたいプログラムも特に今はないし、仕事もそんなに忙しくはないから、思い切ってOneDriveへデータを送受信できる専用ブラウザを作ることにした。

仕様は単純明快。起動したらOneDriveのサインイン画面を表示、メールアドレスを自動入力、あとはEnterキーを叩くだけ。これさえ出来れば、仕事はかなり快適に。

Delphiと力を合わせれば、そんなの1日でできるー!(・・・と、いつも思う)
かくしてOneDrive接続専用Browser作りが日曜日の朝、スタートしたのでした。

2.WebView4Delphiコンポーネントを使う

すぐに思い出したのは、(いつか、いじってみたい)と思っていたWebView4Delphiコンポーネント。

何かでその存在を知り、ダウンロードして、ちょっと触れてみたのは・・・確か、去年のことだった・・・と思いながら、2022年の作業を記録したフォルダ内をさがすとやっぱりあったWebView4Delphiコンポーネント。さっそく、これを今年の作業フォルダへコピーする。試しにdemoフォルダ内のプログラムを動かしてみると、問題なく動く。去年、ダウンロードした際に、コンポーネントのインストールまで行っていたようだ。

WebView4Delphiは、GitHub の salvadordf / WebView4Delphi からダウンロードできる。

salvadordf / WebView4Delphi

https://github.com/salvadordf/WebView4Delphi

Aboutには次の記述があり、

WebView4Delphi is an open source project created by Salvador Díaz Fau to embed Chromium-based browsers in applications made with Delphi or Lazarus/FPC for Windows.

https://github.com/salvadordf/WebView4Delphi

ライセンスはMITだから、利用にあたっては「著作権表示および許諾表示をソフトウェアのすべての複製または重要な部分に記載」すればOK! 面倒なことは一切ない。

MITライセンスの正しい著作権表示および許諾表示の入れ方を教えてくれるWebサイト様もある。なんと有難いことか。作成者の方に心から感謝。

40代からプログラミング!
MITライセンスとは?無料ツール・テンプレートの利用方法と注意点

https://biz.addisteria.com/mit-license-1/

さっそく、WebView4Delphi コンポーネントに添付されている

demos\Delphi_VCL\MiniBrowser

これをベースにして、OneDrive接続専用Browser作りを開始する。

まず、次のようにGUIを作成。メールアドレスの自動入力という「目的」に合わせてVCLコンポーネントを追加する。

「ダウンロードフォルダを開く」は、後で説明。実行時、その右側のチェックボックス「設定」をチェックすると「入力ボタン」より右側のコントロールが出現する。

で、FormCreate手続きに自分用のコードを追加。

implementation

{$R *.dfm}

uses
  uTextViewerForm,
  uWVCoreWebView2WebResourceResponseView, uWVCoreWebView2HttpResponseHeaders,
  uWVCoreWebView2HttpHeadersCollectionIterator,
  uWVCoreWebView2ProcessInfoCollection, uWVCoreWebView2ProcessInfo,
  uWVCoreWebView2Delegates,
  //以下を追加
  Vcl.Clipbrd,
  System.IniFiles, System.UITypes,
  Winapi.ShlObj, Winapi.KnownFolders,
  Winapi.ShellAPI;

procedure TMiniBrowserFrm.FormCreate(Sender: TObject);
var
  Ini: TIniFile;
  strID, strX, strY: String;
begin
  FGetHeaders             := True;
  FHeaders                := TStringList.Create;
  FFileStream             := nil;
  FUserAuthFrm            := nil;
  FResourceContents       := nil;
  FBlockImages            := False;
  FDownloadIDGen          := 0;
  FDownloadOperation      := nil;
  WVBrowser1.DefaultURL   := URLCbx.Text;

  //Formを最大化して表示
  MiniBrowserFrm.WindowState:=wsMaximized;

  //iniファイルの存在を確認
  if FileExists(ChangeFileExt(Application.ExeName, '.ini')) then
  begin
    //iniファイルからデータを読込み
    Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
    try
      strID:=Ini.ReadString('Section', 'ID', '');
      strX:=Ini.ReadString('Section', 'IchiX', '580');
      strY:=Ini.ReadString('Section', 'IchiY', '420');
    finally
      Ini.Free;
    end;
    //復号して表示(実際のプログラムではメールアドレスは暗号化処理している)
    Edit1.Text:=strID;
    EditX.Text:=strX;
    EditY.Text:=strY;
  end;

end;

あと、EXE の実行には、EXE のあるフォルダ内 に WebView2Loader.dll が必要。今回作成するのは64 ビット版の EXE だから bin64 フォルダ内の 64 ビット用DLL を使用しなければならない。

実際の動作の様子。実行時画面では・・・

「設定」にチェックすると・・・

ここにiniファイルから読み取ったサインイン用のID(メールアドレス)と、プログラムからクリックするアドレス入力欄の座標を表示(入力)する。

画面解像度が異なるPCでは、当然、フォーカスを当てたいサインイン用のメールアドレスを入力するフレームの表示位置が異なるから、最初にその位置座標を調べる必要がある。座標Checkにチェックを入れるとマウスポインタの現在位置のスクリーン座標が画面右上にリアルタイムで表示される仕組みだ。

マウスポインタが現在ある位置の座標を取得して表示するコードは・・・

procedure TMiniBrowserFrm.Timer2Timer(Sender: TObject);
var
  lh_Handle:  HWND;
  lpt_Pos:    TPoint;
  lrc_Rect:   TRect;
  lrg_Region: HRGN;
  li_Ret:     Integer;
begin
  if chkZahyo.Checked then
  begin
    //マウスカーソル位置をスクリーン座標で取得
    GetCursorPos(lpt_Pos);
    //自身のウィンドウリージョンを調べる
    lh_Handle := Self.Handle;

    //ウィンドウリージョン取得のため空のリージョンを作っておく
    lrg_Region := CreateRectRgn(0,0,0,0);
    try
      //ウィンドウリージョン取得
      li_Ret := GetWindowRgn(lh_Handle, lrg_Region);
      if (li_Ret <> ERROR) then begin
        //ウィンドウのRectを取得
        GetWindowRect(lh_Handle, lrc_Rect);
        //スクリーン座標からウィンドウの左上を原点とした座標に変換
        lpt_Pos.X := lpt_Pos.X - lrc_Rect.Left;
        lpt_Pos.Y := lpt_Pos.Y - lrc_Rect.Top;
        //ウィンドウリージョン内にマウスカーソルがあるかテスト
        if (PtInRegion(lrg_Region, lpt_Pos.X, lpt_Pos.Y)) then begin
          LabelXY.Caption:=Format('OK %d (%d-%d)', 
            [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end else begin
          LabelXY.Caption:=Format('NG %d (%d-%d)', 
            [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end;
      end else begin
        LabelXY.Caption:=Format('X:%d, Y:%d', [lpt_Pos.X, lpt_Pos.Y]);
      end;
    finally
      DeleteObject(lrg_Region);
    end;
  end;
end;

で、最適なクリックポイントのX座標とY座標を読み取り、Editに入力、保存ボタンをクリックでiniファイルに保存する。

クライアント座標でなく、スクリーン座標としたのは、話をカンタンにするため。だからFormCreate手続きでFormを最大化して表示するように設定している。こうすればどんな解像度のPCでも、画面左上からのスクリーン座標でメールアドレスを入力するフレームの位置が決定できると考えたのだ。

実は、最初は「他のアプリへ文字列を送信」する方法で、サインイン画面を狙い撃ちしようと思っていたのだが、ブラウザの入力欄は「ウィンドウではない」ようで、目印にするハンドルがなく、簡単には文字列を送信できないことがわかった。

朝5時くらいから作業を始めて、半日くらいイロイロ悩んだのだけれど、お昼過ぎにようやくサインインのフレームが表示されている位置をプログラムからクリックして、フォーカスを当て、さらにプログラムからCtrl+V(貼り付け)の操作を行い、iniファイルからクリップボードに読み込んでおいたメールアドレスを流し込めばイイと気づく。

そこで大きく方針を転換。文字列を送信ではなく、サインインのフレームをクリックしてメールアドレスを貼り付ける方向でGUIも、プログラムも準備した。

さらに「保存」ボタンは、メールアドレスとPCごとに異なるサインインのフレームの座標をiniファイルに書き込んで記録するために設置。

これで OneDrive接続専用Browser のGUIは完成。あとはプログラムを書くだけに。

※ 実際のプログラムでは、iniファイルにメールアドレスを保存する際に、さらにひと手間かけて暗号化、iniファイルから読みだす際に復号している。

3.プログラムでクリックを実行

普段、僕がいちばんよく利用するWebブラウザはFirefoxだ。インターネット黎明期、そう誰もが Netscape Navigator を使っていた頃からのファンなのだ。

プログラムを何度も動かして動作確認しているうちに、あることに気づく。それは何かと言うと、OneDriveのサインイン画面を表示した時の挙動が、Firefoxと作成中のOneDrive接続専用Browserではちょっと違うのだ。

FirefoxでOneDriveのサインイン画面を表示した場合は、メールアドレスを入力するフレームにセットフォーカスされた状態でサインイン画面が表示されるのに対し、作成中のOneDrive接続専用Browserではそうならない。しかも、アドレス入力欄を1回クリックしただけではダメ(セットフォーカスされない)で、2回クリックしないと入力待機状態にならない(1回めのクリックで、一瞬セットフォーカスされたように見えるが、その後、キャレットが消失してしまい、点滅状態にならない)。

DelphiのIDEから、Ctrl+F で「SetFocus」を検索キーワードにコード全体を確認しても、それは「見つからなかった」。なぜ、2回クリックしないと入力待機状態にならないのか、その原因はさっぱりわからない。

原因はわからなくても、とにかくそれが現実だから、Webにあった情報を頼りにプログラムからアドレス入力欄をクリックするコードを書いてみた。

動的にマウスをクリックするには?

https://www.petitmonte.com/bbs/answers?question_id=2014

予め、先に示したマウスカーソルの位置座標を調べるコードで、アドレス入力欄の座標を調査・記録しておいて、Formが完全に描画されたところでプログラムコードからアドレス入力欄をクリックする。

  private
    { Private 宣言 }
    //アドレス貼り付け実行の成否
    boolInput:boolean;
    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;

procedure TMiniBrowserFrm.CMShowingChanged(var Msg: TMessage);
var
  dwFlags : DWORD;
  X,Y : Integer;
  //LKeyByte : Byte;
begin
  inherited; {通常の CMShowingChagenedをまず実行}
  if Visible then
  begin
    Update; {完全に描画}
    fgWaitBreak:=False;
    WaitTime(1000);
    //クリップボードを初期化
    Clipboard.Clear;
    //文字列をクリップボードに格納
    Clipboard.AsText:=Edit1.Text;

    dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
    X:=Trunc(StrToInt(EditX.Text)/Screen.Width*65537);
    Y:=Trunc(StrToInt(EditY.Text)/Screen.Height*65535);

    //移動
    Mouse_Event(dwFlags,X,Y,0,0);
    Application.ProcessMessages;

    //クリック
    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Application.ProcessMessages;

    WaitTime(300);

    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Application.ProcessMessages;

    Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);

    // [Ctrl] + [V] のキー操作 -> btnCopyClick手続きで実行
    {
    LKeyByte := Ord('V');
    keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
    keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   0, 0);
    keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   KEYEVENTF_KEYUP, 0);
    keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), KEYEVENTF_KEYUP, 0);
    }
    WaitTime(1500);
    btnCopyClick(nil);
    if boolInput then btnCopy.Enabled:=False;
  end;
end;

WaitTimeなる関数は、次のWebサイト様で紹介されていたものをそのまま使用。

待ち関数の必要性

https://gumina.sakura.ne.jp/CREATION/OLD/COLUMN/CD1MATI.htm
function TMiniBrowserFrm.WaitTime(const t: integer): Boolean;
var
  Timeout: TDateTime;
begin
  //待ち関数  指定カウントが経過すれば True, 中断されたならば False
  fgWaitBreak := False;
  Timeout := Now + t/24/3600/1000;
  while (Now < Timeout)and not fgWaitBreak do begin
    Application.ProcessMessages;
    Sleep(1);
  end;
  Result := not fgWaitBreak;
end;

WaitTime関数の引数の値を様々に変えて実行してみると、MyPCでは、上記の数値で確実にアドレス入力欄をプログラムコードからクリックすることに成功!

メールアドレスをCtrl+Vする部分は、GUIの「入力」ボタンと共用だからボタンのClick手続き側に記述して呼び出す(実行する)ことにする。これだと都合4回、アドレス入力欄をクリックすることになるが、単にクリックするだけだから2回でも、4回でも大差ないだろう。むしろ、確実に動かすための保険だと考え、このままにする。

procedure TMiniBrowserFrm.btnCopyClick(Sender: TObject);
var
  dwFlags : DWORD;
  X,Y : Integer;
  LKeyByte : Byte;
begin

  boolInput:=False;

  try

    //クリップボードを初期化
    Clipboard.Clear;
    //文字列をクリップボードに格納
    Clipboard.AsText:=Edit1.Text;

    dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
    X:=Trunc(StrToInt(EditX.Text)/Screen.Width*65537);
    Y:=Trunc(StrToInt(EditY.Text)/Screen.Height*65535);

    //移動
    Mouse_Event(dwFlags,X,Y,0,0);
    //クリック
    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    //Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
    Application.ProcessMessages;
    WaitTime(500);
    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
    Application.ProcessMessages;

    // [Ctrl] + [V] のキー操作
    LKeyByte := Ord('V');
    keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
    keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   0, 0);
    keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   KEYEVENTF_KEYUP, 0);
    keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), KEYEVENTF_KEYUP, 0);

    boolInput:=True;

  except

    boolInput:=False;

  end;

end;

動作確認すると、ごく、たまーにメールアドレスが自動入力されないこともあるが、9割方予期した通りに動作してくれる。メールアドレスが自動入力されない場合でも、クリップボードにデータは間違いなく読み込まれているから、手動でCtrl+Vすればいいだけだ。

いちいち、メールアドレスを入力することに比べれば、Ctrl+VしてEnterキーを押す方がずっとカンタンだ。イイ感じになってきた。

4.特殊なフォルダを表示する

実際にファイルをOneDriveへ、アップロードしたり、ダウンロードしたり、動作確認を行ってみると、任意のタイミングでダウンロードフォルダを開く機能が欲しくなった。なので、これをボタンクリックで実行できるようにする。

ダウンロードフォルダができたのはWindows Vista以降のようで、この特殊なフォルダへのPathを取得するには、SHGetKnownFolderPath 関数を使うらしい。コードはいつもお世話になるMr.XRAYさんのWebサイトの記事を参考にして書く。

460_特殊フォルダのフルパスを取得

http://mrxray.on.coocan.jp/Delphi/plSamples/460_SpecialFolderPath.htm#08

GUIDの一覧は、次のWebサイトにあった。こちらの情報でダウンロードフォルダのGUIDがわかった。

ファイル ダイアログ ボックスのカスタム プレイス用既知のフォルダー GUID

https://learn.microsoft.com/ja-jp/dotnet/desktop/winforms/controls/known-folder-guids-for-file-dialog-custom-places?view=netframeworkdesktop-4.8

ボタンクリックで、ダウンロードフォルダを開く手続きは次の通り。

procedure TMiniBrowserFrm.btnOpenDLFolderClick(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
  D_FolderPath, ExeFileName:string;
  LhInst:Cardinal;
begin
  //ダウンロードフォルダのGUIDを指定
  FolderID:=StringToGUID('{374DE290-123F-4565-9164-39C4925E467B}');
  if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
  begin
    D_FolderPath := FolderPath;
    //ダウンロードフォルダを開く
    ExeFileName:= 'explorer.exe';
    LhInst:=ShellExecute(Handle, 'open', PChar(ExeFileName), 
      PChar(D_FolderPath), nil, SW_SHOW);
    if LhInst <= 32 then
    begin
      MessageBox(Handle, '起動に失敗しました.', '情報', MB_ICONINFORMATION);
    end;
  end;
end;

5.まとめ

WebView4Delphiコンポーネントのdemoにあるサンプル(MiniBrowser)に、以下の内容を追加したプログラムを使えばOneDrive自動サインインは可能。その手順は次の通り。

(1)プログラムコードでアドレス入力欄をクリックしてセットフォーカス。
(2)クリップボードに送ったメールアドレスを、プログラムコードでCtrl+V。
(3)Enterキー押し下げでOneDriveへ接続。データは即送受信可能になる。

さらに、SHGetKnownFolderPath 関数でダウンロードフォルダへのPathを取得して、エクスプローラで表示すればより一層便利に使えそう。

6.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

面談日程表の自動作成方法

Googleカレンダーを利用して、三者面談の日程表を自動的に作成する方法を学んだ。

【Gooleカレンダーとフォーム】全自動・三者面談の日程調整をつくる

https://www.fy1203.com/2020/03/21/calendar-form/

やりたいこと、そのものスバリの情報に大感謝!
ただ、残念なことに一部、情報が古くなってしまっており、そこでちょっと試行錯誤があったので、最新の情報を含めるカタチで、作成方法をここにメモ。

【今回の記事】

1.専用カレンダーの作成
2.入力フォームの準備
3.スプレッドシートを準備する
4.スクリプトを用意する
5.トリガーを設定する
6.プレビューで動作確認
7.まとめ
8.お願いとお断り

1.専用カレンダーの作成

(1)Googleアカウントがなければ作成。

(2)Googleのトップページの右上のGoogleアプリから、「カレンダー」をクリック

(3)カレンダーが開くので、左下の他のカレンダーを追加「+」をクリック

(4)「新しいカレンダーを作成」をクリック

(5)カレンダーの名前と説明を決めて、「カレンダーを作成」をクリック

2.入力フォームの準備

(1)次は入力フォームです。まず、フォルダを作成しておきます。

(2)Googleドライブの左上の「新規」をクリックして、サブメニューを表示します。

(3)「新しいフォルダ」をクリックして、

(4)フォルダに最適な名前を付けます。この場合、「三者面談」としました。

(5)そのフォルダに入り、「新規」→「Googleフォーム」と順にクリックします。

(6)新しいフォームの作成画面が開きます。

(7)このGoogleフォームで、入力フォームを作成します。タイトルと説明を入力し、画面中央右の設定ボタンをクリックします。

(8)私は、次のように設定してみました(誤りがあるかもしれません)。

(9)設定が終了したら、質問タブをクリック して、画面を切り替えます。

(10)1問目の出席番号は、「プルダウン」から選択するように設定しました。

(11)入力を「必須」にするため、状態を「ON」にします。

(12)質問を追加します。画面右上の 〇囲みの+マークをクリック します。

(13)2つめの問は名前です。次のように「記述式」にして、「必須」は「ON」の状態にします。終わったら、右の 〇囲みの+マークをクリック します。

(14)3つめの問は予約日です。次のように入力します(日付は適当です)。

(15)メニューから「年を含める」のチェックを外します(年を含めるをクリックすれば、チェックが外れます)。

(16)これで入力が「月、日」だけになりました。さらに・・・

(17)4つめの問は開始時間です。「時刻」にして、「必須」は「ON」の状態にします。

これで、フォームの作成は終了です。

3.スプレッドシートを準備する

(1)次に、回答を整理するスプレッドシートを用意します。画面上の「回答」→「スプレッドシートにリンク」をクリックします。

(2)下の画面が表示されるので、次のようにタイトルを入力して「作成」をクリック。

これで、スプレッドシートが作成されます。

4.スクリプトを用意する

(1)画面の表示が「スプレッドシートにリンク」から「スプレッドシートで表示」に変わっていることを確認して、「スプレッドシートで表示」をクリックします。

(2)新しいページにスプレッドシートが表示されます。ここに必要なスクリプトを追加します。

※ 追加するスクリプトは次のWebサイト様で紹介されているものです。

【Gooleカレンダーとフォーム】全自動・三者面談の日程調整をつくる

https://www.fy1203.com/2020/03/21/calendar-form/

(3)スプレッドシートの画面上から、「拡張機能」をクリックして、表示されるサブメニューにある「App Script」をクリックします。(2023年6月現在の操作方法です。ここが上記Webサイト様の記事と異なります。いつの間にか、現在の形式へと変更されたようです。)

(4)新しいページが開き、次の画面が表示されます。

(5)クッキーの使用を許可します。

(6)「無題のプロジェクト」をクリックして、プロジェクト名を「1年A組面談希望調査」に変更して、下の「名前の変更」をクリックします。

(7)次のようなスクリプトの入力画面が表示されます。

(8)次の引用リンク先のWebサイト様で紹介されているスクリプトを1行目から範囲選択して、クリップボードにコピーし、入力画面に(既存のテキスト全体を選択しておいて)上書きします(関数の名前がMyFunctionからsendToCalendarになり、引数(ひきすう)も「空」でなく「e」になります)。

【Gooleカレンダーとフォーム】全自動・三者面談の日程調整をつくる

https://www.fy1203.com/2020/03/21/calendar-form/

(9)コピペしたスクリプトを3か所「確認」または「変更」します。

【その1】

・4行目のYear指定を確認してください。必要であれば、現在の西暦年に変更します。

【その2】

16行目「カレンダーID」を変更します。

最初のカレンダーの画面を開き、画面左の「1年A組三者面談」の右にある「…」をクリックします。

Googleのトップページの右上から、「カレンダー」をクリック。

画面の左下にある「1年A組三者面談」をポイントすると表示される縦の「・・・」をクリックします。

表示されるサブメニューから「設定と共有」をクリックします。

「カレンダーの設定」画面が表示されます。下へスクロールすると、かなり下の方にカレンダーIDがあります。これをコピーします。

コピーしたIDをスクリプトの16行目に貼り付けます。

※ 「***カレンダーIDを入力***」部分とカレンダーIDを入れ替えます。

【その3】

31行目の面談時間を「確認」または「変更」します。

以上でスクリプトの確認と変更は完了です。

5.トリガーを設定する

(1)スクリプトが自動的に実行されるように設定します。画面右にある<>部分をポイントすると、時計のマークの「トリガー」が現れるので、この「トリガー」をクリックします。

(2)トリガー設定画面が開くので、画面右下にある「トリガーを追加」をクリックします。

(3)イベントの種類を「フォーム送信時」に設定して、「保存」をクリックします。

※ この画面で「実行する関数を選択」が空欄になっている場合は、スクリプトのコピペが1行目を含んだ形で(正しく)行われているかどうか、また、スクリプトが保存されているかどうか、確認してください。

(4)次のエラーメッセージが表示された場合は、ブラウザのポップアップブロック(別Windowを自動的に開かない設定がおそらくデフォルトになっている?)を、このトリガー設定ページからの保存操作であれば解除されるように設定してください(操作方法はブラウザにより異なります)。

(5)紐づけるアカウントをクリックします。

(6)My環境では、Googleからの確認画面が表示されました。画面左下の「Advanced(詳細?)」をクリックします。

(7)(必要であれば、画面を下にスクロールして)「unsafe:安全ではない」と表示されている「1年A組面談希望調査」へのリンクをクリックします。

(8)次の画面が表示されます。

Allow(許可)をクリックします。これで入力フォームからGoogleカレンダーへデータが送信されたときに、自動的にスクリプトが実行されるようになります。

6.プレビューで動作確認

(1)画面右上の「プレビュー(目のマーク)」をクリックします。

(2)入力フォームに必要事項を入力して、「送信」ボタンをクリックします。

(3)送信されたメールの内容を確認します。

(4)Googleカレンダーに予約状況が表示されます。

(5)カレンダーに予約状況が表示されない時は、トリガーの設定が正しく保存されているか、どうか、確認してください。(私が最初にテストした際は、Googleカレンダーに予約状況が表示されませんでした。ちょっと焦りましたが、手順を一つひとつ確認して行く中で、トリガーの設定が正しく保存されていなかったことに気づき、この部分の設定作業をやり直したところ、無事、予約状況がGoogleカレンダーに表示されるようになりました。)

7.まとめ

(1)Googleカレンダーを使えば、三者面談自動予約システムは作成可能。
(2)Webには多くの情報があるが、機能の更新が頻繁にあるので時々見直す必要あり?
(3)最終的な面談時刻の確認は、別の連絡手段で確実に行う必要がある。

8.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Reorganization

「再編」

そこに、1台のNASがあった。

時とともに アクセスする人も変わり
記録方法も 場所も いつしか ランダムに・・・

参照したい「データ」 それが「どこ」にあるのか
わかりづらくなってしまった NAS・・・

エントロピーは 必ず 増大するから
長期間に及ぶ運用の結果として 「生まれた」 この状況は
理論的には 正しい ・・・ のかもしれない・・・。

ただ・・・ あの日、確かに・・・

なんとか 出来ないのか?

叫ぶような 声を聞いた。
それは 僕に向けての 声ではなかったけれど・・・

僕は 応えようと 思った。
これまでに培ったネットワークドライブ接続に関する知識。

そのすべてを 賭けて。

【目次】

1.ネットワークドライブ
2.システムエラー1219
3.アカウントに読み書き権限を付与
4.まとめ
5.お願いとお断り

1.ネットワークドライブ

かつて、別セグメントにあるファイルサーバに接続して、その共有フォルダをネットワークドライブとして、マイコンピュータに表示するプログラムを書いたことがあった。

そうだ。あのセグメント越えのプログラムを書いたのは・・・ もう10年以上、前のことだ。

あの時は、ファイルサーバを別セグメントに用意する必要があったけれど。
今、共有フォルダを再構成したいNASは、みんながログオンするセグメント上にある・・・。
なんとか・・・しなければならない敷居は、10年前より、ずっと低い。

もちろん、自分的にいちばん、イイのは・・・ 新しいファイルサーバを用意して、現在のファイル共有の仕組みそのものをイチから作り直すこと、なんだけれど。

でも、それは無理だ。今年度、そんな予算は1円だって計上されてない。
今の環境と機材の中で、なんとかするしか、ない。

今あるNASをなんとか創意工夫して、運用するしか、ないのだ。

( 今のファイル共有を維持したまま、新しい共有環境を作るには、どうしたらいい? )

自問自答を繰り返す。
無理だという前提は一切排除する。
あきらめない限り・・・何とか出来る方法が、必ずあるはずだ。
僕は毎日、あらゆる意味で、いちばんイイ方法を考え続けた。

( 再編する共有フォルダのみ ネットワークドライブとして表示すれば いい )

( 再編に必要なフォルダは 予め用意して・・・ )

( そこへ必要なファイルだけを移動させるんだ・・・ )

朝、出勤途中、クルマを運転しながら、そんな考えが浮かんだ。
ようやく答えに繋がるヒントが見えた・・・ 気がした。

職場に着いた僕は、NASを管理するDSMを起動してみた。前の職場では、自前でサーバ機を用意して、アクティブディレクトリを使ったファイル共有の仕組みを作っていたが、それがたまらなく懐かしくなる・・・。

おいおい アクティブディレクトリ・・・ きみは嫌いだったんじゃ ないか?

自分の気持ちの変化に驚きながら、管理画面を見ていて( あれ? )って思った。

( リサイクルフォルダがものすごい容量を喰ってる・・・ )

・・・ってことは、このNAS自体のバックアップの仕組みがどうなっているのか、それはわからないけれど、とにかくNAS全体でゴミ箱の設定が有効になってるわけだ。よくよく見ると一般ユーザーはそこにアクセスできないけれど、管理者にはそれができるようだ・・・。もしかして、バックアップの代わりに、ゴミ箱を有効化してる・・・?

それなら、話は簡単だ。
バックアップ用の媒体を別に用意して、そちらに日々のバックアップをとり、リサイクルフォルダを使わない設定に変更すればいい。これでNASの空き容量を増やせるはずだ。

そうだ。どこかにバックアップさえ、きちんと作れたら・・・
ファイルサーバを新しく用意しなくても、なんとか、なるんじゃないか?

そう思った瞬間、この問題の完全な解決方法が「見えた」気がした。

リサイクルフォルダを使わない設定にすれば、今より確実に空き容量は増える。で、NASのルートに、これまで使われていない「共有」って名前のフォルダを新しく作り、その下に、クライアントPCのマイコンピュータにネットワークドライブとして表示する新しいフォルダ群を用意して、必要なフォルダとファイルだけ、そこに「コピーではなく、移動」すれば、現在のファイル共有環境を生かしたまま、新しいファイル共有環境を同じNASの中に再構成することが出来るんじゃないか・・・。

お金と手間をかければ、もっといい方法もあるのかもしれないが、それが無理な現状ではおそらく、これがベストに近い解決策なんじゃないか? ・・・そう思えてきた。

で、バックアップは・・・ どこへ とればいい?

取り敢えず、僕の手元には、前任者から引き継いだ、用途を限定せずに利用できる空き容量1TBのSSDがある。これに最も重要なデータのバックアップをとろう。で、事務方の責任者に必要十分な容量のバックアップメディアがどうしても必要なことを説明して、理解が得られたら、速やかに可能な限り大容量のバックアップ用HDDを購入してもらおう。

それから、万一の火災等の事故への対応も考えなければならない。
バックアップのバックアップは、どこに、作ればいい?

僕は、先日、複合機のスキャナーでスキャンした画像データを出力する設定を行ったばかりの・・・ 別の部署にあるNASにかなりの空き容量があったことを思い出した。

新しく再編する共有フォルダだけをバックアップするなら、あのNASを利用すればなんとかなるんじゃないか?

これで、だいたいの見通しが立った。あとはやるだけだ。

まず、バックアップ(のバックアップ)用途に使いたいNASの保管場所を、安全な場所に変えなければならない。現在でも夜間はアラームのかかる部屋に、そのNASは設置されているのだが、これを24時間、施錠された部屋に移設することにした。

幸いにしてバックアップ(のバックアップ)用途に使いたいNASが設置されている部屋は、最初から情報処理用途に準備された部屋なので、床下がネットワークの配線に使える。

床の四角いカーペットを剥いで、その下の床板を外し、LEDライトを片手に、24時間施錠された小部屋までの経路を探ってみる。

床下に障害となるような構造物はない。なんとか、なりそうだ。竹製の1m物差しを何本も用意して、床下に差し入れ、LANケーブルを物差しの先端に養生テープで固定して、鍵のかかる小部屋へ向けて1本、2本とそれを養生テープで繋ぎ、少しずつ、慎重にLANケーブルを送る。

無事、LANケーブルは小部屋へ到達。
なんで汗まみれになるのか、知らないが。

バックアップ用のNASのユーティリティの起動方法がわからないので、覚悟を決めて、NASの電源ボタンを長押し。するとWebの解説にあった通り、Beep音が鳴ってNASはシャットダウンされた。

このNASには、もう一つ、何か別用途でのNASが接続されていた。シャットダウン時、果たしてどうなるか心配したけれど、そちらも同時に電源が切れた。なんだか、わからないけれど、取り敢えず二つのNASの電源は連動しているようだ。

NASに繋がっている電源とLANケーブルを全て外し、二つのNASをこの上なく大切に抱えて、24時間施錠された小部屋へ移設する。

鍵のかかる小部屋の床には、幸いにして電源コンセントが用意されていた。部屋の状況から判断しておそらく、以前はここにサーバ機が置かれていたのだろう・・・。

雷対策が施された電源の延長コードをコンセントに差し込んで、これにNASの電源ケーブルを繋ぐ。

続けてLANケーブルも接続。
接続状態に問題がないことを何度も確認し、祈るような気持ちで、NASの電源スイッチをONにする。

何事もなかったかのように、2台のNASが無事、再起動した。
これでハードウェアの準備はOKだ。

次は、NASの共有フォルダをネットワークドライブとして各クライアントPCのマイコンピュータに表示する、オリジナルプログラムを用意しなくてはならない。

僕は以前にDelphiで書いたネットワークドライブ接続のプログラムをバックアップ用のHDDから探し出し、プロジェクトを My PC のデスクトップにコピーした。

Delphiを起動すると、十数年前に作った、懐かしいGUIが現れた。

現在の状況に合わせて、必要な部分を書き換える。
ユーザーが自分の自由意思で、メイン画面からスタートアップに登録できるようにする。
あと、IDとPasswordも暗号化してイニシャライズファイルに保存。プログラム起動時に自動的に読み込んで表示するように設定を変更。

このNASの共有フォルダをネットワークドライブとして表示するプログラムの「接続」ボタンにたどり着くためには、ユーザーは生体認証とPIN入力の2段階認証を潜り抜ける必要があるから、この設定で、セキュリティ的に問題はないはずだ。

ネットワークドライブ接続のインターフェイス

このプログラムを書いた時は、別セグメントにあるファイルサーバに接続できるよう、確か、RouteADDコマンドを使ってクライアント機のルーティングテーブルを書き換えたんだ・・・。で、その際、設定の変更を残す(=記録する)pオプションはわざと指定せずに、シャットダウンすれば自動的に設定が、ルーティングテーブルを書き換える前の状態に戻るようにしたんだ。

このRouteADDコマンドを実行するBATファイルを、プログラム内部で生成して動かすところがすごく難しかったんだ・・・。RunAsAdmin・・・ 確か・・・夏に思い立って、プログラムが完成したのは秋が深まったころだった。

今回、接続したいNASは同じセグメント内にあるから、PCのルーティングテーブルを書き換える必要はない。NASの共有フォルダをクライアント機のマイコンピュータにネットワークドライブとして表示するだけだから、その手続きはより簡単に済む。

あの時、表示する共有フォルダは、「個人フォルダ」・「校内共有」・「校務分掌」・「教科」に設定した気がする。今回、個人フォルダはどうするか・・・?

NASの設定を調べてみるとラッキーなことに、新しくユーザーアカウントを作るとhomesフォルダにそのユーザー専用のホームフォルダが用意されることがわかった。試しに「test-u」というアカウントを作成してみると、homesフォルダに「test-uフォルダ」が確かに出来ている。これを「個人フォルダ」ネットワークドライブとして表示すればいい。

NASのルートには「共有」という名前のフォルダはなかったので、早速それを作成し、その下に「校内共有」・「校務分掌」・「教科」の各フォルダを準備する。あとは接続プログラム側で各共有フォルダへのPathを接続情報として指定すればOKのはずだ。

この画面を表示するにはパスワードが必要

DSMで確認したら、NASのドメイン名は指定されていなかった。koumu.localとでも設定しようかと思ったが、現状の変更を最小限に留めておくことにし、やめておくことにした。

ネットワークドライブを設定して表示するプログラムは次の通り。

procedure TForm1.Button1Click(Sender: TObject); // ネットワークドライブの接続
var
  PW,ID:string;
  //iniファイル読込み
  Ini: TIniFile;
  strW,StrX,StrY,strZ:String;
  strDomainName:String;
  //ネットワークドライブ名変更
  X,D:Variant;
  InfoStr1,InfoStr2,InfoStr3:string;
  //スタートアップに登録
  MyObject : IUnknown;
  MySLink  : IShellLink; // ShlObj
  MyPFile  : IPersistFile; // ActiveX
  Directory : String;
  WFileName : WideString;

const
    MyRegFile : string = 'Software\Microsoft\Windows\CurrentVersion\Explorer';
    MyMessage : string = 'スタートアップに登録しますか?';
    MyFolders : string = 'Startup';

begin

  //接続先ドメイン名を取得
  //iniファイル読込み
  Ini:=TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //実際にはマスターパスワードを暗号化文字列から復元している
    MasterPassW:='XXXXXXX';
    //テキスト暗号化設定情報を読込み
    strDomainName:=暗号化文字列から複合する関数(Ini.ReadString('セクション', 'DomainName', 'デフォルト値'),
      MasterPassW, False);
  finally
    Ini.Free;
  end;

  //【準備作業】Password,IDを確認して変数へ取得
  try
    //カーソルを待機状態に変更
    Screen.Cursor:=crHourGlass;

    //UserName(ID)確認
    if Edit1.Text='' then
    begin
      MessageDlg('IDが無効です!', mtInformation, [mbOk] , 0);
      Edit1.SetFocus;
      Exit;
    end else begin
      //有効なドメイン名がある場合
      //ID:=strDomainName+'\'+JTrim(Edit1.Text);
      //ドメイン名がない場合
      ID:=JTrim(Edit1.Text);
    end;

    //パスワードを確認
    if Edit2.Text='' then
    begin
      MessageDlg('パスワードが無効です!', mtInformation, [mbOk] , 0);
      Edit2.SetFocus;
      Exit;
    end else begin
      PW:=JTrim(Edit2.Text);
    end;
  finally
    Screen.Cursor:=crDefault;
  end;

  //【第1段階】ルーティング情報の設定を実行

  // 今回接続するNASは同じセグメントにあるので
  //ルーティングテーブルの書き換えは不要

  //【第2段階】ネットワークドライブを追加

  //カーソルを待機状態に変更
  Screen.Cursor:=crHourGlass;

  //ドライブ設定情報読込み
  //iniファイル読込み
  Ini:=TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //マスターパスワード
    MasterPassW:='XXXXXXX';
    //ShowMessage(MasterPassW);
    //テキスト暗号化設定情報を読込み
    strW:=暗号化文字列から複合する関数(Ini.ReadString('セクション', 'W_Drive', 'デフォルト値'), MasterPassW, False)+JTrim(Edit1.Text);
    strX:=暗号化文字列から複合する関数(Ini.ReadString('セクション', 'X_Drive', 'デフォルト値'), MasterPassW, False);
    strY:=暗号化文字列から複合する関数(Ini.ReadString('セクション', 'Y_Drive', 'デフォルト値'), MasterPassW, False);

    if ComboBox1.Text<>'' then
    begin
      //ComboBox1が空欄でなければ教科のドライブにも接続
      strZ:=EDText(Ini.ReadString('セクション', 'Z_Drive', 'デフォルト値'),
        MasterPassW, False)+ComboBox1.Text;
    end else begin
      StrZ:='';
    end;
  finally
    Ini.Free;
  end;

  //ネットワークドライブを切断
  NetDel;

  //ネットワークドライブ接続確認用変数を初期化
  NetDrvError:=False;

  try

    //Userへの通知
    ProgressBar1.Visible:=True;
    if ComboBox1.Text<>'' then
    begin
      ProgressBar1.Max:=4;
    end else begin
      ProgressBar1.Max:=3;
    end;
    ProgressBar1.Position:=0;

    //ネットワークドライブ(個人フォルダ)を追加
    AddNetworkDrive('W:', strW, '', PW, ID);
    if not (NetDrvError) then
    begin
      //ネットワークドライブ名を変更
      X:=CreateOleObject('Shell.Application');
      D:=X.NameSpace('W:\');
      D.Items.Item.Name:='個人フォルダ';
      ProgressBar1.Position:=ProgressBar1.Position+1;
    end else begin
      NetDrvError:=False;
      MessageDlg('Error:個人フォルダに接続できません!', mtError, [mbOk] , 0);
      Exit;
    end;

    //ネットワークドライブ(校内共有)を追加
    AddNetworkDrive('X:', strX, '', PW, ID);
    if not (NetDrvError) then
    begin
      //ネットワークドライブ名を変更
      X:=CreateOleObject('Shell.Application');
      D:=X.NameSpace('X:\');
      D.Items.Item.Name:='校内共有';
      ProgressBar1.Position:=ProgressBar1.Position+1;
    end else begin
      NetDrvError:=False;
      MessageDlg('Error:校内共有に接続できません!', mtError, [mbOk] , 0);
      Exit;
    end;

    //ネットワークドライブ(校務分掌)を追加
    AddNetworkDrive('Y:', strY, '', PW, ID);
    if not (NetDrvError) then
    begin
      //ネットワークドライブ名を変更
      X:=CreateOleObject('Shell.Application');
      D:=X.NameSpace('Y:\');
      D.Items.Item.Name:='校務分掌';
      ProgressBar1.Position:=ProgressBar1.Position+1;
    end else begin
      NetDrvError:=False;
      MessageDlg('Error:校務分掌に接続できません!', mtError, [mbOk] , 0);
      Exit;
    end;

    //ネットワークドライブ(教科)を追加
    if ComboBox1.Text<>'' then
    begin
      AddNetworkDrive('Z:', strZ, '', PW, ID);
      if not (NetDrvError) then
      begin
        //ネットワークドライブ名を変更
        X:=CreateOleObject('Shell.Application');
        D:=X.NameSpace('Z:\');
        D.Items.Item.Name:=ComboBox1.Text;
        ProgressBar1.Position:=ProgressBar1.Position+1;
      end else begin
        NetDrvError:=False;
        MessageDlg('Error:'+ComboBox1.Text+
          'フォルダに接続できません!', mtError, [mbOk] , 0);
        Exit;
      end;
    end;

    if not (NetDrvError) then
    begin
      //接続ボタンを使用不可に設定
      Button1.Enabled:=False;
      //接続状態の表示を設定
      Label4.Caption:='状態:接続中';
      Label4.Transparent:=False;
      Label4.Color:=clLime;
    end;

  finally
    Screen.Cursor:=crDefault;
    ProgressBar1.Position:=0;
    ProgressBar1.Visible:=False;
  end;

  //【第3段階】最終処理

  //コンピュータを開く
  ShellExecute(Handle, 'Open','EXPLORER.EXE','::{20D04FE0-3AEA-1069-A2D8-08002B30309D}','',SW_SHOW);

  //タスクトレイへ常駐する手続きを呼び出し
  MovetoTasktray;

  //カーソルを元の状態に変更
  Screen.Cursor:=crDefault;

  Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //設定情報を初期化
    InfoStr1:='';
    InfoStr2:='';
    InfoStr3:='';
    //テキスト暗号化
    InfoStr1:=暗号化文字列を作成する関数(Edit1.Text,Unit1.MasterPassW, True);
    InfoStr2:=暗号化文字列を作成する関数(Edit2.Text,Unit1.MasterPassW, True);
    InfoStr3:=暗号化文字列を作成する関数(ComboBox1.Text,Unit1.MasterPassW, True);
    //ネットワークドライブ設定情報
    //iniファイルに保存
    Ini.WriteString('セクション', 'UserID', InfoStr1);
    Ini.WriteString('セクション', 'UserPW', InfoStr2);
    Ini.WriteString('セクション', 'UserSubject', InfoStr3);
  finally
    Ini.Free;
  end;

  //スタートアップに登録
  {
  if Application.MessageBox(PChar(MyMessage),'確認',
    MB_YesNo + MB_IconQuestion) = IdNo then exit; // Noなら何もしないで終わり
  }

  if chkStartup.Checked then
  begin
    //Yesなら
    MyObject := CreateComObject(CLSID_ShellLink);
    MySLink  := MyObject as IShellLink;
    MyPFile  := MyObject as IPersistFile;
    MySLink.SetPath(PChar(Application.ExeName));
    with TRegIniFile.Create(MyRegFile) do
    try
      Directory := ReadString('Shell Folders',MyFolders,'') + '\';
      WFileName := Directory + Application.Title + '.Lnk';
      MyPFile.Save(PWChar(WFileName),False);
    finally
      Free;
    end;
  end;

end;

ネットワークドライブの割り当て手続きは次の通り。

procedure AddNetworkDrive(Drive, UNC, Comment, Password, UserName: string);
var
  NetResource: TNetResource;
{$IFDEF UNICODE}
  user, pass: PWideChar;
{$ELSE}
  user, pass: PChar;
{$ENDIF}
begin
{$IFDEF UNICODE}
	with NetResource do
  begin
  	dwType := RESOURCETYPE_DISK;
    lpLocalName := PWideChar(Drive);
    lpRemoteName := PWideChar(UNC);
    lpComment := PWideChar(Comment);
    lpProvider := nil;
  end;
  if (Password = '') then
    pass := nil
  else
    pass := PWideChar(password);
  if (UserName = '') then
    user := nil
  else
    user := PWideChar(UserName);
{$ELSE}
	with NetResource do
  begin
  	dwType := RESOURCETYPE_DISK;
    lpLocalName := PChar(Drive);
    lpRemoteName := PChar(UNC);
    lpComment := PChar(Comment);
    lpProvider := nil;
  end;
  if (Password = '') then
    pass := nil
  else
    pass := PChar(password);
  if (UserName = '') then
    user := nil
  else
    user := PChar(UserName);
{$ENDIF}
  if (WNetAddConnection2(NetResource, pass, user, 0) <> NO_ERROR) then
  begin
    //エラー発生時の処理
  	NetErrorProc(GetLastError);
    NetDrvError:=True;
  end else begin
    //エラーが発生しなかった場合の処理
    NetDrvError:=False;
  end;
end;

ネットワークドライブの接続解除手続きは次の通り。

procedure RemoveNetworkDrive(Drive: string);
begin
  if (Drive = '') then exit;
{$IFDEF UNICODE}
	WNetCancelConnection2(PWideChar(Drive), 0, true);
{$ELSE}
	WNetCancelConnection2(PChar(Drive), 0, false);
{$ENDIF}
end;

ネットワークドライブを切断する手続きは次の通り。

procedure TForm1.NetDel;
begin
  //Network Driveを切断
  RemoveNetworkDrive('W:');
  RemoveNetworkDrive('X:');
  RemoveNetworkDrive('Y:');
  RemoveNetworkDrive('Z:');
end;

プログラムをコンパイルして出来たexeをクラウド経由で支給されたノートPCへ送った僕は、プログラムを起動して、必要事項を入力し、祈るような気持ちで「接続」ボタンをクリックした・・・。

テスト用のユーザーアカウントを作成して接続実験を行った

2.システムエラー 1219

こんなエラーメッセージが表示された(記憶を頼りにエラーを再現した画像)

最初から上手く行くとは思っていなかったけど、やはりエラーは心に痛い。しかも、数字しか表示されてないから、接続プログラム内に僕が予め用意したエラーの通知文にはないエラーだ。

procedure NetErrorProc(err: DWORD);
var
  s: String;
begin
  case err of
    ERROR_ACCESS_DENIED:  s := ERR_ACCESS_DENIED;
    ERROR_ALREADY_ASSIGNED:  s := ERR_ALREADY_ASSIGNED;
    ERROR_BAD_DEV_TYPE:  s := ERR_BAD_DEV_TYPE;
    ERROR_BAD_NET_NAME:  s := ERR_BAD_NET_NAME;
    ERROR_BAD_PROFILE:  s := ERR_BAD_PROFILE;
    ERROR_BAD_PROVIDER:  s := ERR_BAD_PROVIDER;
    ERROR_BUSY:  s := ERR_BUSY;
    ERROR_CANCELLED:  s := ERR_CANCELLED;
    ERROR_CANNOT_OPEN_PROFILE:  s := ERR_CANNOT_OPEN_PROFILE;
    ERROR_DEVICE_ALREADY_REMEMBERED:  s := ERR_DEVICE_ALREADY_REMEMBERED;
    ERROR_EXTENDED_ERROR:  s := ERR_EXTENDED_ERROR;
    ERROR_INVALID_PASSWORD:  s := ERR_INVALID_PASSWORD;
    ERROR_NO_NET_OR_BAD_PATH:  s := ERR_NO_NET_OR_BAD_PATH;
    ERROR_NO_NETWORK:  s := ERR_NO_NETWORK;
    //次の行はエラーメッセージから調べて追加
    53:               s := ERROR_BAD_NETPATH;
    1200:             s := ERROR_BAD_DEVICE;
    2202:             s := NERR_BadUsername;
  else
    s := IntToStr(err);
  end;
  MessageDlg(s, mtError, [mbOk], 0);
end;

Google先生にお伺いをたてると・・・

「システム エラー 1219 同じユーザーによる、サーバーまたは共有リソースへの複数のユーザー名での複数の接続は許可されません。」とのこと。

なんのこっちゃ? と思ったが、さらに調べてみると「Windows資格情報」が既に登録されているとこのエラーが発生するらしいことがわかった。そこで「コントロール パネル ⇨ ユーザー アカウント ⇨ 資格情報マネージャー」の順に辿って、Windows資格情報を確認するとNASのIPアドレスとともに、僕のIDとパスワードが登録されていた。いちばん最初にNASに接続した際に自動的に登録されたらしい。これを取り敢えず、削除してみる。

Windows資格情報を初期化

で、ID:test-uで再チャレンジするが「システムエラー1219」が再度表示され、NASの共有フォルダはネットワークドライブとして表示されない。

実在する一般ユーザーのアカウント設定を参照して作ったテスト用ユーザーだから、設定に間違いがあるとは思えないのだが・・・。

ふと、思い立って(=揮発性メモリにWindows資格情報が残っているためかと考えた、ここでいったんPCを再起動すれば、古いWindows資格情報は消えるはず・・・)Myアカウントで試してみる。僕のアカウントは管理者用のアカウントで何でもできるから、テストには不向きと考え、敢えて使わなかったのだ。

Myアカウントを入力して、接続テストを実行。すると・・・

無事、NASの共有フォルダへの接続に成功!
期待通りにネットワークドライブとして表示できたが、空き容量表示の色が「赤」なのが痛々しい・・・。

3.アカウントに読み書き権限を付与

Myアカウントなら繋がることはわかったので、ひと安心したが、なぜ一般ユーザーアカウントで繋がらないのかがわからない。まさか、全ユーザーのアカウント設定を管理者に昇格させるわけにも行かず(入れないフォルダがあるわけではないので、それでも運用上は特に問題は起こらないと思うのだが)、何としてもその原因を確かめないといけない。

DSMを起動して、ユーザーを選択し、「編集」⇨「権限」でMyアカウントと一般ユーザーアカウントの違いを見比べてみる。違いは一目瞭然。Myアカウントには「homes」と「共有」に「読込み/書込み」があるが、test-uアカウントにはそれがない・・・。

これかー!!

DSM ⇨ コントロールパネル ⇨ グループ と辿って、何か適当なグループはないか検討してみると、職員全員と説明のあるグループを発見。さっそくこれを編集して、「homes」と「共有」に「読込み/書込み」権限を付与(設定)する。

続けて、test-uアカウントに「職員全部」が所属するグループを追加する。

これでtest-uアカウントは「homes」と「共有」各フォルダに対する読み書きが可能に。

今度は、test-uアカウントでも無事接続でき、NASの共有フォルダがネットワークドライブとして表示された。やった。目標を実現できた!

あとは、このプログラムを含むネットワーク環境改善案を全体に提案して共通理解を持ち、共有資産を再編すればいい。

クライアントPCの数は100に満たない。これくらいなら僕ひとりで接続プログラムの導入と設定は十分可能だ。

接続プログラムを動かして、ネットワークドライブが見えている状態であれば、Windows資格情報が消されていても、揮発性メモリには接続先のIPアドレスだけでなく、ユーザーIDとパスワードも書き込まれるらしく、クライアントPCのデスクトップ上にあるNASへのショートカットも機能することがわかった。

作業の途中、NASのルートはネットワークドライブに指定できない(何らかの共有フォルダを指定しなければならない)という事実を初めて知り「愕然」とする瞬間もあったが、ネットワークドライブ接続後にデスクトップにあるNASへのショートカットが機能すれば何の問題もない。共有資産の再編作業は滞りなく実行できるはずだ。

クライアントPC1機を複数人で運用するのであれば、Windows資格情報を残したままだと他の人が接続しようとした時に「システムエラー1219」が発生するのは間違いないが、現状一人一台の生体認証でログインするクライアントPCだから、マシンを割り当てられた職員以外の人の使用は考えにくく、Windows資格情報は消去せずにそのままにしておいても大丈夫かもしれない。

いずれにしても、明日以降、試験的に運用しながら、もし問題点があるようならそれを発見・改善し、組織全体がよりよくなれるよう、力を尽くそう。

この方法がいちばん良い方法であるとは思えないが、今の僕にできるベストであることは間違いない。ならば、自分にできるいちばんよいことをする。それをずっと繰り返すしか、ないじゃないか・・・。

4.まとめ

現在のファイル共有環境を生かしたまま、新しいファイル共有環境を再構成するには、現在のファイル共有システムの中に、新しい共有フォルダを準備してそこへ古い環境から必要なファイルだけを移動する方法がよいのではないか? と今回の経験から思った。実際の運用は、これからだけれど・・・

5.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Supports Zero-Starting!

ゼロ始まりに対応!

どんな理由があって、そうなったのか知りませんが、マークシート方式で行われる予定の大学入学共通テストサンプル問題、教科「情報Ⅰ」の解答の選択肢は、始まりが「1」ではなく、「0」からになってます・・・。

ディジタルのイメージから、「0」・「1」とつながる0始まりにしたのか?
それとも他に何か0から始めなければならない必然的な理由があるのか?
サンプル問題を作成した方に、その理由をぜひ伺ってみたい気もしますが・・・

今回は、My MarkSheet Reader を、解答の選択肢「0」始まりに対応させたというお話です。

【今回の記事】

1.なんで0始まりなの?
2.教科「情報」用マークシートを作成
3.ゼロ始まりに対応
4.まとめ
5.お願いとお断り

1.なんで0始まりなの?

大学入学共通テストの理科基礎科目は例外なく、解答の選択肢の始まりは「1」から。
物理も、化学も、生物も、地学も、全部、それが「1」になってるってのは、「選択肢の始まりは1から」って、誰もがそう感じるから・・・だと思うのですが。

教科「情報」では、なぜかそれが「0」からになってる・・・。

検索してみると、そのことに注意を促すWebサイトが複数見つかります。
例えば、選択肢の番号が「0」始まりであることに気付かず、「121」とマークしたつもりが実際には「010」だった。思い込みにはくれぐれも注意しましょう!・・・みたいな。

僕は、僕のマークシートリーダーをよくする云々ではなく、今回だけは、この無視できない現実にとにかく「対応する」ことにしました!

2.教科「情報」用マークシートを作成

まずはマークシートそのものを準備しなければいけません。これが理科の場合なら、選択肢の数は8個もあれば十分です。しかし、教科「情報」では、例えば著作権関係の内容を見てみると、著作者の権利として、氏名表示権、同一性保持権、複製権、上演権、演奏権、公衆送信権、口述権、展示権、頒布権、貸与権、翻訳権、翻案権の12の権利があることがわかります。これを解答の選択肢として準備する場合があり得ますから、選択肢の数は少なくても15程度、十分な余裕を持って設定した方がよさそうです。

幸いなことに僕のマークシートリーダーは、数学での利用を想定して選択肢数は最大16個まで対応できるように作成してあります。そこで、数学用のマークシートを改造して教科「情報」用のマークシートを作成することにしました。

Wordで作成した数学用マークシート

まず、行番号の「アイウエオ・・・」を「12345・・・」に書き換えます。

次に、マークを0始まりで15まで、16個用意します。
1行分作成したら、あとはひたすらコピペします。
これを3列分繰り返して、1列25問×3で75問に対応できるマークシートとしました。

列の行番号が半角のカタカナから、半角の数字2桁になったため、解答欄座標を取得するマーカー■■■(トリプルドット)の位置に関して若干の修正が必要でしたが、何とか思った通りの形に仕上げることができました。

完成した教科「情報」用マークシート

出来れば、問題数は100問まで対応可能としたいところですが、用紙サイズの関係もあり、75問でよしとすることにしました。どうしても75問以上の設問が必要な場合は、数学用途に2枚1セットで採点できるようにプログラムを組んであるので、それを活用すればなんとかなります。まぁ・・・試験時間60分なら、最大75問に対応していればOKでしょう。

完成した教科「情報」用マークシート

とりあえず、マークシートは完成です。ここまでは極めて順調に推移しました。次は、いよいよプログラムの改良です。

3.ゼロ始まりに対応

どのようにプログラムを改良しようかと考えた時、一瞬、教科「情報」専用のマークシートリーダーにしようか・・・と思ったのですが、やはり、そうではなく、1つのプログラムで様々な教科・科目に対応できる方が理想だと思い直し、選択肢の始まりは「0」とするか、「1」とするか、ユーザーが選べるようにプログラムを改良することにしました。

ユーザーが選択肢の始まりを「0」「1」のどちらかに設定できるよう改良

設定欄に、最初の選択肢の番号を指定するVCLコントロールを設置するスペースを何とか作成し、そこにComboBox1個とLabelを2つ貼って、上の図のように選択肢の始まりの番号を指定できるようにしました。もちろん、ここで指定した番号は必要であればイニシャライズファイルに保存して、次回起動時も有効化されるように設定。このComboBoxの名前は思いついたまま適当に「cmbOneZeroSelect」として、読み取りコードを次のように改良します。

  begin
    //StringGridに読み取り結果を表示
    //オリジナルのプログラムは1行で終わってた
    //StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
    //選択肢の0始まりに対応できるようコードを改良
    if cmbOneZeroSelect.Text='1' then
    begin
      StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
    end else begin
      if (strAnsList[intSG_k]='99') or (strAnsList[intSG_k]='999') then
      begin
        StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
      end else begin
        strAnsList[intSG_k]:=IntToStr(StrToInt(strAnsList[intSG_k])-1);
        StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
      end;
    end;
    ・・・
  end;

早速、実行してみました!

数学用のマークシートを読み込ませて動作確認

よかった☆ 期待通りに動作してくれました!

ちなみに「999」は「空欄」、「99」は「複数マークあり」を意味します。
Gridコントロールをクリックすれば、当該箇所のマーク状態をチェックできます。
赤い色の矩形は、プログラムを実行した際の採点結果チェック実行時に、画面上に実際に表示される矩形です。
サンプル画像を3枚読むのに要した時間は772ミリ秒

1行16選択肢で、1列に25行、これが1枚に3列あるから、マークの数は合計1200個/枚あります。MyPC環境では、マークシート1枚について1200あるマークを約250ミリ秒で読み取ってますから・・・。1秒でほぼ4枚、1クラス40名分なら約10秒で読み取り完了です。

自分で言うのもなんですが、結構、高速に動作しているんじゃないか・・・と。

ただ、10万枚くらいは手作業で採点できそうな時間を開発に費やしてますが・・・。

4.まとめ

ようやく、大学入学共通テストの教科「情報」に対応したマークシートリーダーが出来ました。解答欄の選択肢の始まりが「0」か、「1」か、ただそれだけのことなのですが、両方にきちんと対応するのは、やっぱりそれなりに大変でした。

5.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

This updated is support for use with high resolution devices

高DPIに対応しました!

これまでずっとPC画面の解像度は1366×768に固定して、この解像度での使用のみを前提に、僕はプログラムを書いてきた・・・。僕のプログラムが走るマシンは全部、この解像度だったから、それで何も問題は起きなかったのだけれど。

【今回の記事】

1.2880×1920の世界を知る
2.Formの表示がたいへんなコトに・・・
3.問題点を続々と発見!
4.exeを高DPI対応に設定
5.VCLの幅や高さを自動調整
6.まとめ
7.お願いとお断り

1.2880×1920の世界を知る

新しく支給されたPCの画面解像度は2880×1920で、拡大縮小率は150%に設定されてた。持ち運ぶことを考えると、ノートPCの画面サイズはそうそう大きく出来ないから、画面サイズが変わらないまま、解像度だけ上がってしまうと、相対的にアプリや文字の見た目はどんどん小さくなって、目にとてもやさしくない画面になる。だから拡大150%や拡大200%って設定が必要なんだと思うけど・・・。

取り敢えず、この環境で僕のプログラムを動かすとどうなるか、実験してみた。

1366×768、拡大率100%で表示した場合(開発時の設定)
2880×1920、拡大率100%で表示した場合

高解像度画面では、ボタンのCaptionが読めない・・・。

2.Formの表示がたいへんなコトに・・・

しかも、このプログラムから別のFormを呼び出すと・・・

たいへんなコトに・・・

自分的には、こう表示されてほしいのですが・・・
(今までは何の問題もなく、こう表示されていた)

この(今までの)ように表示するには、どうしたらイイ?

やばい。何としても高DPIに対応させないと、職場のみんなにプログラムを使ってもらうどころか、自分ひとりですら使えない。画面が高解像度になっただけで、こんな問題が生まれるなんて・・・。これまで考えたこともなかった。

けっこうショックが大きくて、心がまた折れかけたけど、この問題をクリアすれば、プログラムも、僕も、もっとよくなれるんだって、必死で自分に言い聞かせる。

3.問題点を続々と発見!

高解像度画面で一通り、プログラムの動作検証を行ってみると、見つけられただけで次のような問題が発生することがわかった。

① Formが設計時とは異なる大きさで表示される。
② 画面表示の拡大設定を行わないと、字が読めないくらい小さくなる。
③ 拡大設定時には、VCLコントロール(Toolbar)の幅や高さが意図しないものになる。

まず、①の問題の解決にチャレンジ。

FormCreate手続きでFormの幅を指定しても無駄。
まるで言うことを聞いてくれない。
いったいナニがどうなると、この問題が発生するのか?
これまで、こんな問題に出会ったこと、ないぞ・・・。

そう思いつつ、いろいろ調べてみると、次の情報を発見。

フォームを新規作成したらまずやる事 (Delphi)

https://ht-deko.com/ft1004.html#100408_02

明らかな既視感があったので、以前、どこかで見た情報に間違いないと思うのだけれど、知識として使ったことがなかったので、情報の有用性に気づいてなかった・・・。

この中に、Scaledプロパティに関して、次の記述が・・・

Scaled
常に False。True にすると OS の DPI (ユーザが指定した DPI) によってフォームサイズやコントロールサイズが勝手に変更されてしまいます。

(たぶん、コレだ・・・)

早速、すべてのFormのScaledプロパティをFalseに変更。なんでこんな問題を起こすような設定がデフォルトでTrueなんだ?・・・何か、大切な理由でもあるんだろうか?

動かして確認。

直った!(・・・というか、壊れなくなった)

これで①の問題は解決。思ったより簡単に解決できて、よかった!

4.exeを高DPI対応に設定

②の文字の大きさについて、Google先生にいろいろきいた結果、こちらもベストと思われる対応方法を発見。

Windows11でアプリやメニューが小さい時に行う高DPI設定

https://win11lab.info/win11-high-dpi/

設定方法は、次の通り。

exeを右クリックして表示されるサブメニューの「プロパティ」をクリック
「高DPI設定の変更」をクリック
「高い DPI スケールの動作を上書きします」のチェックボックスをチェックして、
拡大縮小の実行元は「システム」を選択。

で、OK → 適用 → OK と順にボタンを押して画面を閉じ、アプリを再起動すると、Formが適正な倍率で表示されてアプリやメニューが見やすくなった。ちなみに「システム」ではなく、「アプリケーション」では表示に変化がなく、「システム(拡張)」ではFontが高解像度化された感じに。

いちどexeにこの設定を実行しておけば、画面の解像度をいろいろ変更しても常にFormは適正な大きさで表示されるようになり、たいへん便利!

Windowsには、ほんとうにいろんな画面解像度の設定があるから、exeに対するこのおまじないは必須なのかもしれない・・・。これで②の問題も無事解決。

結局、②の問題は、プログラムではなく、OS側の設定の問題だった。

5.VCLの幅や高さを自動調整

最後に残った③の問題に取り組む。まず、これがどういう現象かと言うと・・・

手書き答案採点プログラムで、画面を横にスクロールさせるために作ったToolbarコントロールが、本来なら次のように表示されるはずなのに・・・

ToolButton1,2,3とBevel1の4つのコントロールの幅の合計値がToolbar1の表示サイズの幅となるはず

上の4の設定を行わず、かつ、画面の拡大縮小が100%でない場合には・・・

表示そのものが崩れてしまう・・・

職場のマシンたちは全部!デフォルト設定が「高解像度」で、画面の拡大率150%だから、何にもしないで僕のプログラムを配布されたままの状態で動かしたら、間違いなく、この問題が発生してしまう・・・。

マジ、困った・・・。

すがるような思いでGoogle先生に援けを乞う。すると・・・

03_高 DPI における画像の描画サイズ調整

http://mrxray.on.coocan.jp/Delphi/Others/DisplayDPI_Image.htm#03

またしても、Mr.XRAYさんのサイトに救いとなる情報を発見!

職場では、僕のことを「困った時の〇〇さん・・・」と呼ぶ人がいるけど、
僕にとってMr.XRAYさんは、「本当に困った時のMr.XRAYさん」です。

これまでにいったい何度、僕の窮地を救ってくださったことか・・・。
あらためてMr.XRAYさんに、心から感謝のありがとうです。

Mr.XRAYさんのホームページにあった情報をもとにプログラムを次のように修正。

procedure TFormCollaboration.btnSelectClick(Sender: TObject);

  //--------------------------------------------------------------------------
  //  ディスプレイの拡大縮小の比率を取得
  //  100% の時は 1.0.150% の時は 1.5 を返す
  //--------------------------------------------------------------------------
  function GetDpiRatio: Extended;
  var
    LXDpi : Integer;
  begin
    LXDpi := GetDeviceCaps(GetDC(0), LOGPIXELSX);
    Result := LXDpi / USER_DEFAULT_SCREEN_DPI;
  end;

var
  ・・・
  //高DPIに対応する
  VCL_Width:Extended;
  VCL_Height:Extended;

begin

  ・・・

  //解像度が変わると不具合がでる
  //r.Right := r.Left+ToolBar1.Width;
  //r.Bottom := r.Top+ToolBar1.Height;

  //解像度の変更に対応
  //幅
  VCL_Width := (ToolButton1.Width + 
    ToolButton2.Width + ToolButton3.Width + Bevel1.Width) * GetDpiRatio;
  r.Right := r.Left + Trunc(VCL_Width);
  //高さ
  VCL_Height := ToolBar1.Height * GetDpiRatio;
  r.Bottom := r.Top + Trunc(VCL_Height);

  ・・・

end;

GetDpiRatio関数を使ってディスプレイの拡大・縮小の比率を計算し、これをVCLコントロールの幅と高さに掛けて、コントロールが適切に描画されるように設定。

上記設定を行った後、実行中のToolbar
(フローティング状態で画面の任意の位置に埋め込む)

こうしてプログラム側でも、VCLコントロールの幅や高さを画面の拡大縮小に合わせるように設定しておけば、exeそのものに「高 DPI 設定の変更」を設定しなくても・・・

ちょっとカタチは崩れるけど、使えないレベルではない。
exeに「高 DPI 設定の変更」を設定せず、
拡大150%で実行してみた場合

これで③の問題も無事解決できた!

プログラムも、僕も、よくなれた☆

それは間違いない・・・から、いいんだけれど。
ひとりでも、戦えるかな・・・

Let me see you through.

空を見上げて・・・

I’m missing you.

そう思えてならない時が、あるんだ。

6.まとめ

(1)様々な画面解像度に対応するには、FormのScaledプロパティをFalseに設定。
(2)画面の拡大縮小に対応するにはプロパティの「高 DPI 設定の変更」を利用。
(3)画面の拡大縮小にプログラムコードでも対応可能。

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Let’s mount a counterattack from now on!

反撃を開始するぞ!

【今回の記事】

1.色が消えた!
2.BYODがあった
3.反撃開始!
4.お願いとお断り

1.色が消えた!

そのマシンを僕たちはCIJP3号機と呼んでいた・・・。
いちおう、解説すると、3機めのColor Inkjet PrinterだからCIJP3号機。規定の印刷契約枚数まではカラー・モノクロ問わず、同一料金で印刷可能な、愛すべきマシンだったが・・・。

今、僕の隣に CIJP3号機はいない。

あのマシンさえ、あれば・・・。この2週間、何度、その思いを繰り返したか、わからない。
気がつけば空を見上げて。なつかしい人たちの面影を思い浮かべて、涙し、大量印刷可能なマシンは、モノクロの輪転機しかない環境に涙し、毎日、心が折れに、折れまくった。

大量カラー印刷。それが「当たり前」に出来た世界から、「まったく」出来ない世界へ来たのだから、そう思うのは当然だけど。

正直、なんとかなるんじゃないか・・・と、あまく考えていた部分もあって・・・。
とにかく、このままでは命懸けで作った My Secret Weapon※1 が使えない。
マークシートリーダーは、カラー印刷ができなくてもまだなんとかなりそうだけれど・・・。手書き答案採点プログラムの返却用答案画像だけは・・・「赤」が使えないと、どうにもならない。

※1「赤」で採点記号を答案画像に印字する、手書き答案の採点プログラムのこと。

例えがよくないかもしれないが、心境的には、高性能(?)ミサイルはあるんだけれど、その発射装置が破壊されたってところだ。

せめて2色印刷※2ができないか・・・?

※22色印刷は、普通「黒」+カラー1色(例:赤)で行う印刷のこと。フルカラー印刷に比較すれば、ローコストでインパクトのある資料が作成できる。

そう考え、取り敢えず事務方の責任者にそれとなく伺ってみたが、現状、難しいではなく、「無理」である とのこと。しゅんとして孤独な作業部屋に戻り、また、空を見上げて、心に涙の雨を降らす。空は・・・あんなに青く、美しく、晴れて、きれいなのに。

なぜ・・・。

僕は聞きたいよ。これまでのすべてが、嘘だったって、思えばいいの?

やりたいことは、〇や × 、合計点を赤で印刷したいだけなんだけど・・・。

きっと、月面に一人取り残されて、太陽光の散乱のない、真っ暗な空に浮かぶ青い地球をみたら、こんな気持ちになるんじゃないか・・・。

モノクロームの世界から眺める、フルカラーの地球。
あぁ、僕はあそこに住んでいたんだって・・・。

たのしかったなー。(T_T)

こうなったら、いっそのこと・・・

たかが250万だろ?
個人的に購入する分には何の問題もない はずだ。
思い切って、マシンを買うか?

でも、よく考えろ・・・
Delphi の使用許諾ライセンスが10個分買えるぞ。

バカ!
そんなにDelphi買って、どぉすんだ?

Object Pascalの信者は、絶滅危惧種の生存個体数グラフのように減少の一途をたどっているはずだ・・・

(それは〇県の〇科(科目名:〇)の教員数も同じだと思うが・・・)

だから Embarcadero Technologies, Inc. さんの営業収益増に貢献することはきみの悲願だろ? Delphiの未来がかかってるんだ・・・

あぁでも、今は・・・カラー印刷がどぉしてもしたいんだ。
A社の営業担当の方の名刺は、大切に保存してある。
電話して『ボク、1台買います!』って言えば、
それで済む話じゃないか。

おまえには、そのガッツと勇気がないのか・・・

(それは「勇気」ではなく、「無茶・無謀」だよ:亡くなった祖母の言葉)

ばぁちゃん、ごめんよ。
オレ、やっぱり大人になんか、なれないよ・・・

モノクロームの世界では、誰も気にしないことで、また、オレはこんなに悩んでる。なぜ、毎回毎回、オレばかり、誰も悩まないことで悩むんだ。

ふと THE STREET SLIDERS の、あの名曲が心に浮かぶ・・・

この曲、リアルタイムで聴いてたんだ・・・。
確か・・・大学2、3年の頃だから、もう30年も前のことだ・・・。

Please get out of my mind.
Please get out of my mind.
抜け殻になっちまうからさ。

Please get out of my mind.
Please get out of my mind.
何処かへ、消え失せてくれ。

THE STREET SLIDERS 「GET OUT OF MY MIND」より引用

あぁ・・・どうせ折れた心だ。
抜け殻のような気持ちを抱えて、
ひとりで苦しむのは、もうたくさんだ・・・

こんな消え失せて欲しい現実なんかに負けてたまるか・・・
七転八倒の人生でいいじゃないか・・・
何回倒れたって、オレは起き上がって見せる・・・

ここから抜け出すんだ。
今までだって何とかしてきたじゃないか・・・
そうだ・・・ ここに色がないのなら、
オレが色をつくってやる。

この色の消えた世界に・・・

2.BYODがあった

挫けそうな気持ちを抱えたまま、起案文書を書く。どうしても・・・、どうしても、このままでは終われない。My Secret Weapon が「もし使える状態」になれば、働き方改革にも大いに貢献できるはずだ。僕にできる唯一の社会貢献じゃないか。それに・・・

『僕がこの世から消えた後でも、動くプログラムを創る』

この夢を叶えないうちは死ねない・・・。なにか・・・、なにか、いい方法はないか・・・。
また、くすんだ窓ガラスの向こう側の空を見上げる、気がつけば、この2週間ですっかりそれがクセになってしまった・・・。

ふと、空よりずっと近いところ、天井に無線LANのアンテナが見えるのに気づく・・・

あの部屋の前の廊下にも・・・
その隣の部屋の前の廊下にも・・・
その隣の隣の部屋の前の廊下にも・・・それが・・・ある

そうだ。BYOD(Bring Your Own Device)環境があった。
カラー印刷の「紙」じゃなくて、「画像データ」、または「PDF文書」を、個人所有のタブレットに送信するんだ・・・。せっかく整備したBYOD環境の活用にもつながるし・・・。なにより「紙」を大量に消費するという、My Secret Weapon の最大の弱点も解消できる!

最高の解決方法だ!
なんとかなりそうだ!!

3.反撃開始!

スキャナーは幸い、使い慣れたものと同機種の複合機が、これまた幸い、僕の自由になる環境下に1台あった。ただ、スキャンした画像の転送先が設定されていなかったので、記憶を頼りにNASの中に適当なフォルダを作成して、そこに転送されるよう設定。セキュリティの部分でちょっと引っ掛かったけど、なんとか、クリア。そのへんにあったA4の印刷物を使ってテストした結果も良好。これで答案の読み取り準備はOKだ。

次は、僕のプログラムの改良。

まずは、マークシートリーダーで使用するExcelのマクロだ。今のままでは紙への印刷しかできないから、CheckBoxを追加して、これにチェックマークを入れて印刷ボタンをクリックしたら、採点結果を通知するPDFファイルを作成するようにすればイイ。

VBA(Visual Basic for Applications)は、そんなに詳しくないけど、Object Pascal よりメジャーだし、参考資料はWebに山のようにあるはずだ。きっと、なんとか、なる・・・

チェックボックスを追加して、キャプションを「PDFに出力する」に設定

TabIndexも忘れずに変更。開始番号のテキストボックスのTabIndexが0、終了番号のテキストボックスは1、追加したチェックボックスは2、印刷実行のコマンドボタンは3、キャンセルボタンは4に設定する。

なんで、そうなってるのか、理解に苦しむんだけど、Delphiと違って、VBAでは「標準仕様」で、なんにもプログラムコードを書かなくてもEnterキー押し下げでフォーカスがTabIndexで指定したコントロールへ「勝手に」移動する。そのようなonKeyPress イベントが予め実装されているようだ。

Enterキー押し下げで、他の動作をさせたい場合はどうするのか知らないけど、便利と言えば便利な「標準仕様」であることは言うまでもない。でも、Delphiみたいな、すべてをプログラマが制御できる環境に慣れると、VBAの「標準仕様」には驚きというか、恐怖に近いものを感じてしまう。これは僕だけのことだろうか・・・。

この動作はDelphiのObject Pascalなら、次のコードを書かなければ実現できない。

//TEditのonKeyPress イベント
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
  //Enterキーで次のコントロールへ
  if key = #13 then begin
    keybd_event(VK_TAB,0,0,0);
    Key := #0;
  end;
  //入力制限する場合 ここに記述する

end;

Form1のKeyPreview を True にする必要があったと思うが、状況によっては、これも使える。

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin
  //リターンキーで移動させる
  if Key = #13 then
  begin
    SelectNext(ActiveControl, True, True);
    Key := #0;
  end;
end;

だから、僕が作ったVBAのGUIをマウスなしで操作するなら、設計時にコントロールを移動したい順番にTabIndexを指定しておき、実行時には、まず開始番号をテキストボックスへ入力してEnter、次に終了番号をテキストボックスに入力してEnter、チェックボックスへフォーカスが勝手に移動したら、スペースキーを押し下げてチェックをON/OFFし、Tabキーを押してフォーカスを印刷実行のコマンドボタンへ移動。Enterキー押し下げで、印刷(or PDFファイル作成)が実行という手順になる。

Windows標準のコントロールの移動は、Enterキーではなく、Tabキーであることを忘れてはいけない。

印刷&PDFファイル作成部分は・・・

Private Sub CommandButton1_Click()

    Dim PrintNo1 As Integer
    Dim PrintNo2 As Integer
    Dim i As Integer
    
    'PDF出力用に追加
    Dim Rng As Range
    Dim fName As String
    
    If UserForm1.TextBox1.Text = "" Then
        MsgBox ("開始番号を半角数字で入力してください。")
        TextBox1.SetFocus
        Exit Sub
    End If
    
    If UserForm1.TextBox2.Text = "" Then
        MsgBox ("終了番号を半角数字で入力してください。")
        TextBox2.SetFocus
        Exit Sub
    End If
    
    PrintNo1 = UserForm1.TextBox1.Text
    PrintNo2 = UserForm1.TextBox2.Text
    i = PrintNo1

    For i = PrintNo1 To PrintNo2
        Range("A2").Select
        ActiveCell.FormulaR1C1 = i
        Range("B6:AB38").Select
    
        ActiveSheet.PageSetup.PrintArea = "$B$6:$AB$38"

        'PDFにする範囲を指定
        Set Rng = ActiveSheet.Range("B6:AB38")

        'PDFファイル名
        If i < 10 Then
            fName = Range("C8") & "0" & Range("E8") & "_" & Range("G8")
        Else
            fName = Range("C8") & Range("E8") & "_" & Range("G8")
        End If

        '全角&半角スペースを削除する
        fName = Replace(fName, " ", "")
        fName = Replace(fName, " ", "")
        
        If Not CheckBox1.Value Then
            '紙に印刷(テスト時にはここをコメント化する)
            ActiveWindow.SelectedSheets.PrintOut Copies:=1, Collate:=True
        Else
            'PDF出力
            Rng.ExportAsFixedFormat Type:=xlTypePDF, _
            Filename:=ActiveWorkbook.Path & "\" & fName & ".pdf"
        End If
        
    Next i
    
    Range("A2").Select

End Sub

なんでVBAなのに、PDF作成部分の代入演算子(?)が := なんだ?って疑問が・・・生まれたけど、取り敢えず、動かす方が先。

=と:=の違い

http://officetanaka.net/excel/vba/beginner/02.htm

上の疑問について、調べてみた結果、VBAで、引数に値を設定するときに使用される記号が「:=」であるとのこと。初めて知りました。

サンプルデータだから、超簡略化して・・・

これで実行すると・・・

こうなると、ボタンのCaptionは「実行」だけの方がいいかな?

結果は・・・、次の通り。

できた! できた!!

PDFファイルの内容は・・・

あとは、このPDFファイルをBYOD環境を利用して、個人所有のタブレット端末へ送信する仕組みを構築すれば、いい。それは仲間と協力すれば、必ずできる。

魂の抜け殻みたいな状態の・・・今の僕でも、取り敢えず、ここまでは・・・できた。
明日、朝、目が覚めたら、手書き答案採点プログラムの合計点印刷手続きを改良して、「紙」へ印刷するのではなく、上で行った作業と同様に、今度は指定フォルダ内に返却用答案画像ファイルを作成&保存できるようにしよう。アルゴリズムは同じだから、Delphiでも必ずできるはずだ。

プログラムも、僕も、よくなるんだ・・・。
それで、いいじゃないか。
また、ゼロから創めればいいだけのことじゃないか・・・。

今までだって、全部ゼロから作ってきたんだ。
そうだ。僕は・・・ 精一杯、やった。
その時々で、僕にできることは全部やった。
後悔など、何ひとつない。

それが今、また、ゼロに戻っただけだ。

この・・・色のない世界に、色を創るのは僕だ。
僕にしか、出来ない仕事だ。
僕が色を創るんだ。

僕があきらめない限り、
この夢は・・・実現できる可能性を残している。

僕にしか、出来ない仕事をしてみせる・・・。

あきらめるもんか。
やるぞ!

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

タンデム・シート

2023年3月31日(金)、僕は自由になった。

だったら、4月1日(土)は、

どこへ行こうか・・・
何をしようか・・・

先週の終末は、雨だった。
今週末、この地域は・・・晴れの予報だ。
それなら土曜日の、いちばん、は・・・ 16歳の頃から決まってる。

ずっと雨だったから、止まったままの・・・
ダブル・オーバー・ヘッド・カムシャフト・・・ インライン4。
そうだ。僕の空冷四気筒DOHCエンジンに、火を入れよう。

右のリアショックからは、オイルが漏れてる・・・
30年モノだもの、壊れないほうが不思議。

空冷の四気筒エンジンが、当たり前に存在した時代があったことを、
聞き伝えではなく、原体験として語れるのは・・
もしかして、僕らの世代が・・・最後、なんじゃないか・・・?

30年を・・・さらに越えて、時はこんなに流れても・・・
きみの見た目は、なんにも、変わらない。

初めて出会った、あの日のままさ・・・。
タンクも、それから、エンジンも、まだ輝きを残してる。

でも、お互い、年齢を重ねた・・・。
ウソだろって・・・本気で、思うよ。
出会って、もう、30年・・・。

燃料チャージはキャブレターだから、駆けだすには十分な暖気運転が必要だし、
例え、オイルが熱くなっても、今はもう、1万回転以上は怖くて回せない・・・

きみを壊したくない気持ちは、もちろん、本当だけど。
僕も、もう、若くないんだ。身体がエンジンの回転数に追いつかない・・・
いつも、動きが一瞬遅れるのを感じる・・・それも、本当なんだ・・・。

僕だけの偏見かな?
きみが大好きな理由さ・・・

キャブレター吸気のエンジンには・・・、うまく言葉に出来ないんだけれど、
インジェクター仕様のエンジンには絶対にない、楽しさ があるんだ。
チョークを引いて、掛けるエンジンなんて・・・
もしかしたら今は、知らない人のほうが、多いんじゃないか。

「掛かるかな・・・?」って、スターターを廻す度に感じる、ドキドキ感。
そして、掛かった瞬間の、言葉にできない・・・うれしさ。

きみのエンジンに、もし、キックペダルがあったら、
エンジンを掛ける度に・・・僕は泣き崩れていた・・・かも、しれない。

こんなに好きなのに、
もう二度とラインアップされることのない・・・
空冷四気筒DOHCエンジン。

・・・だから、僕は、繰り返してしまう。

きみへの想いと、
過去と、
現在とが、
交差する時を。

雨の日に、火酒を片手に、これまでにいったい何時間・・・
きみのエンジンを眺めたことだろう。

晴れた日には、16歳だった・・・、あの日に還って。
きみのエンジンの咆哮を、何度、たまらない想いで・・・聴いたことだろう。

お互いに年齢を重ねた・・・だから、スライダーもありかな・・・って

こんなに美しい機械を、僕は他に知らない。
これを造ったひとは、いったい、どんなひとなんだろう。

焼けたオイルの匂いが好き

憧れてたんだ・・・

『あいつとララバイ』の研二くんの駆る『Z2』に・・・

僕のは・・・1990年2月発売の KAWASAKI ZEP400 Type C2。
オリジナルのメーターはC3以降のと違って、砲弾型じゃなかったから、
SUZUKIのバンディットのを、無理やり、くっつけた。
心配した車検も、このままで、通った!

正直、C3以降のオリジナルよりインパクトがあるんじゃないかな?
このメーター。あんまりないでしょう?
白い文字盤と、オレンジの指針の組み合わせが、好きなんだ。

別の日のスナップ。オイルが漏れていたリアショックをオーリンズに交換。

ミラーも角ばったノーマルのじゃなくて、
丸いZ2タイプのショートミラーに変えた。
おかげで、後ろは、なぁーんにも見えなくなったけど、気にしてない。
昔は、右側だけ付けてたけど・・・、規則では左もないとダメみたいだ・・・。

ウインカーは、オリジナルのはプラスチックが劣化して折れちゃったから、
一昨年、ずっと憧れてた・・・、小さなヨーロピアンタイプのに、替えた。

ステップは、BEET 工業の『スーパーバンク・バックステップ』
ノーマルのは、おとなしすぎたから、ステップ位置を少し後ろ、少し、上へ変えた。
タンデムステップが付くのが、うれしかった・・・。

彼女は、真夜中に一度だけ、乗ってくれた。
あれからタンデムは1回もしてないけど・・・。

ずっとセパレートだった・・・ハンドル。
取り回しと、何より車検がたいへんだから、今はノーマルに戻した。
あの頃は毎週末、上野のバイク街に、通いつめて、夢に見たカタチを探したんだ。

セパレート・ハンドルにしてた頃は・・・
トップブリッジ外して、ハンドル付けて・・・
タンクに干渉しないか、何度も確かめてから、ハンドル位置を決めて・・・

ウインカーやチョークの操作ユニットがセットできるよう、
そうだ。ハンドルに穴を開ける加工を、ドリル片手にやったんだ・・・。

ガレージのそこだけ、光が当たって、輝いてる感じだった。

組み上がったハンドルを握った、その瞬間。
今でも覚えてる。

きみと、ひとつになれた・・・気がした。

マフラーは、定番の、モリワキの直管。
焼けて錆びる度に、サンドペーパーをかけて、耐熱塗料を塗り直した・・・。
アクセルを開ければ・・・車検対応って、マジ、ウソだろ・・・ってくらいの・・・いい音。
3速、8000回転あたりのエキゾースト・ノートが最高・・・気持ちイイ。

雨の日は絶対に乗らないから、後ろのフェンダーは取り外した。
ナンバーを取り付けるための代替部品は、厚さ2mmのアルミ板を半日かけて、糸鋸で切り出して作った。気が遠くなるような作業だったけど・・・

すごく、楽しかったな・・・。

サイドカウルとシールも、テールカウルも、それから、尾灯も・・・
全部、Z2タイプの、それに、変えた。

もちろん、タンクのエンブレムは旧字体の KAWASAKI に。
古い両面テープはギター用のオレンジ・オイルで剥がした。

あの夜は・・・ ガレージの中が、いい匂いだったな・・・。

全部、よく見なければ、わからないけど。
自己満足だから、イイんだ。

オイルクーラーは、今はノーマルだけど・・・
前は EARL’S のそれを付けてた。
ホースの部品が劣化して割れちゃってから、交換して、そのままになってる・・・

認めたくなんか、ないけど・・・
そうだ、確かに、30年という「時」が、流れたんんだ。

時に、激しく、
時に、静かに。

きみが、赤茶けたサビなんて、まだ知らずに輝いてた、あの日から・・・

今は、もうお互いに、時代遅れさ・・・。でも、それでいいじゃないか・・・

永遠にとまるな・・・
きみの鼓動。

僕は・・・ きみが、大好きだ。

4月1日の空は、気持ちよく、晴れた

『タンデム・シートに乗らないか・・・』

 隣で、クルマのハンドルを握る彼女に、そう、聞いてみた。

『怖いから、イイ』

 きみは、覚えてるか・・・

 菜の花が、咲いてたよな・・・
 すごい、気持ちよかった。

 彼女が待つ場所へ、
 きみと走った・・・。

 あの日を。

自己一致

幼い頃は、欲しかったおもちゃを買ってもらえたら、もうそれだけで幸せでした。
ひとは成長するにつれて、欲しいものを手に入れたり、内なる希望を実現するには、苦労を伴う努力が必要なことを知ります。

ひとがなぜそのように成長するのか?
あくまでも「僕にとって」という但し書き付きなのですが、その答えを知る「きっかけ」となったのがマズローの心理学との出会いです。

Abraham Harold Maslow(アメリカ合衆国の心理学者、1908 – 1970)

また、すべての物事には、始まりに必ず、何らかの「きっかけ」があり、「きっかけ」を得て初めて、ひとの「心」に変化が起きるわけですから・・・このプロセスが非常に重要で、ひとの想いと行動を考える上では、物事の始まりとなる「きっかけ」は、絶対に見落としてはならない、重要な要素であると思います。

マズローは自身の発表した欲求5段階説で、人間の欲求を「生理的欲求」「安全の欲求」「社会的欲求」「承認の欲求」「自己実現の欲求」の5つの階層に分類し(後に、自己実現の欲求の、さらに1階層上に「自己超越の欲求」を付け加えますが)、人はより低次元の欲求が満たされて初めて、より高い次元の欲求の実現を求めるようになると、定義しました。

【マズローの欲求5段階説】

6.自己超越の欲求・・・自らを犠牲にしてもよいから、貢献する/理念の実現を図る。
5.自己実現の欲求・・・自分らしい自分を生きるために満たしたい欲求。
4.承認の欲求・・・他者から認められたいという欲求。
3.社会的欲求・・・人間社会の集団に所属したい欲求(所属と愛情の欲求とも)。
2.安全の欲求・・・安心かつ安全な環境の中で暮らしたいという欲求。
1.生理的欲求・・・睡眠等、生きるために最低限満たされる必要がある欲求。

ひとの成長期の前半から後半にかけて、このマズローの欲求5段階説の3階層目及び4階層目である「社会的欲求」や「承認の欲求」が現れ、ひとはその欲求を満たすため、様々に努力します。

社会の仕組みそのものが、人間の発達段階に合わせて作られているから、ちょうどこの時期に入試や入社・採用試験が集中するのは、当然です。

現実社会には様々な矛盾があり、現実が理想に一致しない状態・・・そう、私たちが「ふしあわせ」と呼ぶ状態が存在することも、私たちはこの時期に(否応なく)知ることになります。もちろん、それとは逆に、自らが理想とする状態と、現実が合致している状態が「しあわせ」であることも・・・。

そのような意味での「しあわせ・ふしあわせ」は、「周囲から見て、ある基準と比較して相対的に判断される」ものではなく、あくまでも本人の中での「理想」と「現実」の一致、いわゆる「自己一致」の有無のみによって決まるものと言えます。

だから、常人には耐え難い苦痛を伴うことが明白なオリンピックのマラソン競技でも、それに出場することを目指した選手にとっては、オリンピックに出て金メダルを取ること(自らの理想)と、それを実現するために苦しい練習が必要なことは当然(現実)だから、彼らが日々行う「一般人から見れば苦痛以外の何ものでもない過酷な練習」も、本人的には、その必要性において完全に「自己一致」しています。この「自己一致」があるから、マラソンの選手は、普通の人ならば確実に死んでしまうような練習を毎日普通に行い、しかも、その苦痛に満ちた状態を「しあわせだ!」と表現できてしまったりするわけです。

10代後半における、所属と承認の欲求の実現の具体例をひとつ挙げよと問われたら、多くの人が、その最たるものは「大学入試である」と答えるのではないでしょうか?

次に示すのは、ある教員が書いた先輩教師への手紙です。

最近、こんな出来事がありましたので、お伝えしたく、筆を取りました。

昨年、秋が深まった頃、模擬試験の問題がわからないと、質問に来てくれた子がいました。

出来る限り、丁寧に解説して、理解が深まるようアドバイスしたのですが・・・、解説の最後に「わかったかい?」と、そう訊ねると、その子は俯いたまま、涙をポロポロこぼして言いました。

(どんなに努力しても、模試を何回受けても、毎回同じ点数で成績が全然上がらない)

(いったい、どうすれば応用問題が解けるようになるんですか?)

あぁ・・・ 僕でよければ、代わりに試験を受けてあげたい。
そう、思ったのですが、それはできません。

(このまま、この子を帰すわけにはいかない。今、自己一致させるには、どうすればいい・・・)

僕は、死に物狂いで考えました。
そして、こう、言いました。

「わからない問題があったから、もっとよくなりたくて、きみは質問にきた。
 今、きみのやってることは、間違いか。それとも、正しいことか、どっちだい?」

その子は、泣きながら、でも、はっきり答えました。

「正しいことです。」

「ならば、それを続けるしかないじゃないか。
 わからないことがあれば、悩んでる暇なんか、ない。すぐにおいで。
 きみがわかったと言うまで、きみにつきあうぞ。
 僕もわからない問題なら、わかるまでいっしょに考える。
 それだけは、絶対に、約束する。」

「もし、それでダメなら、僕を一生恨んだらいいじゃないか。」


今年度の大学入学共通テストが行われた翌週の月曜、放課後、
午後5時を過ぎてあたりがすっかり暗くなった頃、
その子が僕の準備室をたずねてくれました。


(できたかい?)

ほんとうは、そう言いたかったのですが、この場合、そうは言えません。

「全力を出し切れたかい?」

僕は、そう訊ねました。

「はい。47点取れました。先生のおかげです。」
(この科目の満点は50点です)

(僕は、何もしていない・・・)

僕は、本当に、何もしていない。ただ、できる限り、丁寧に、その子の質問に答えただけなのですが、人は本当に不思議な生き物です。

どうしてこんなにも、胸が熱くなるのか、その理由を、僕は、今も、言葉にできません。

手紙の中の彼女が「ある教員」のところへ質問に来た時、彼女の中に「自己一致」がありませんでした。つまり、応用問題が解ける状態(=理想)と、応用問題が解けない状態(=現実)の不一致が、彼女の「不幸な状態」を作っていたわけです。

「ある教員」は、彼自身のこれまでの経験から、彼女の中で「自己一致が失われている」ことが苦しみの最大の原因であることに気づき、短時間で、何とかして、それを作り出そうとします。

現実の中で「よくなりたい」から質問に来た。それを確認することで、引き続き「よくなる」ためにはそれが最善の方法だと再確認がとれ、自らの行いの正しさを再認識することで、現実的には「まだ応用問題が解けるようにはなっていない」にもかかわらず、彼女の中に「この正しいことを繰り返して行けば応用問題が解けるようになるかもしれない」という「希望」が生まれます。すると現実的には「何ひとつ変わっていない」のに、「不幸せ」だった状態が「自分は正しい」という「幸福」な状態に変化します。

これが「自己一致」です。

さらに「ある教員」は、彼女の特別な味方であることを強調しています。相手の弱さや苦しみを自分の中に見て、相手の行動が正しい場合には、肩を並べ、ともに生きる姿勢を示すことで、相手の中に「生きるちから」を再生することができます。

最後の「一生恨んだらいい」には、カウンセリング的な意味はありません。
「ある教員」独特の個性でしょう。

相手への最大限の誠意の上に、カウンセリングの知識と技法を乗せれば、相手に「しあわせ」をプレゼントすることができます。これが「言葉の魔法」です。

ここに書いたことに加えて「共感的理解」・「受容と許容」について学べば、より一層知識を深めることができます。

この物語の彼女は、卒業式当日、「ある教員」へ手紙を書いてきてくれました。

 あの日、私の中で何かガラっと変わった気がします。

たったひとつ、自己一致が生まれた、それだけでひとはこんなにも強くなれるのですね。

現在、長時間労働等の問題がクローズアップされ、教員志望者が減少し、教員不足が深刻化していると聞きました。教員の仕事は、日々、自らの学びを深め、技能を向上させることで、児童・生徒のみなさんのためだけでなく、職場の同僚に対しても貢献でき、人間として感じられるこれ以上ない「よろこび」と「感動」に出会える仕事だと思います。

教員希望者のみなさん! がんばってください。
世はきみが立つのを待っています!!

【お断り】

この物語に登場した「彼女」は実在する、とてもすてきな女の子です。
本人の掲載許可を得た上で、ここに書かせていただきました。
また、このエピソードはある「公」の場で語られたものであり、個人を特定してその秘密を公開する意図はなく、法的な守秘義務にも反しないものと判断して掲載しています。

私にとって、愛とは・・・

なぜ、ひとに「悲しみ」という感情があるのか・・・
僕はその理由を知りません。

悲しみなんて、すべてなくなればいいのに・・・ と、そう願いながらも、
悲しみのない世界がどんな世界なのか、
僕には、その世界を想像することができません。

もし、悲しみがなくなれば・・・
ひとは、ほんとうにしあわせになれるのでしょうか・・・

この問いかけが許されるなら、神なる存在に、そう尋ねてみたい気がします。

「愛」もそうです。

ただ、「悲しみ」とは違い、「愛のない世界」を知ることはできます。
さまざまな作家が「愛のない世界」を描いていますが、僕の心に最も残った作品は、遠藤周作さんが「女の一生 二部・サチ子の場合」で描いた、第二次世界大戦中、ポーランドの古都クラクフ郊外にあった「アウシュビッツ強制収容所」の風景です。

「ここに・・・・・・愛があるのかね、神父さん」
ひきつった声でその囚人は笑った。
「あんた、本当にそんなことを信じているのか」

「ここに愛がないのなら・・・・・・」
と神父はかすれた声で言った。
「我々が愛をつくらねば・・・・・・」

新潮文庫「女の一生 二部・サチ子の場合」 遠藤周作 著より引用

なぜ、ナチス・ドイツはその「収容所」を作ったのか。
ユダヤの人々に対する、ヒトラーの「歪んだ思い」はどこから生まれたのか。
そんな彼を、ドイツの人々は、なぜ「支持」したのか。
また、彼は実際に「どのような政治」を行ったのか。

もしかしたら、そのような(教科書に書かれていないことも含めて)歴史の背景を学んでから、この作品を読んだ方がいい・・・のかもしれませんが、たとえ、そうでなくても・・・

ここに登場する・・・マキシミリアン・コルベ神父の言う「愛」が容易くないことは明らかです。容易い「愛」などないと、今は思いますが、自らの内なるものの何が「愛」なのか、僕はずっと理解していませんでした。自らの内なるものの何が「愛」であるのかを知り、その重さを初めて理解したのは、二十歳を過ぎてからのことでした。

人生では、縁も、所縁もない人と、偶然同じ場所で、同じ時を生きることがあります。そして、その隣人と生涯をともにするわけでも、なんでもないのに、その人の「現在」を例えようもなく、重たく感じることがあります。

例えば、学校の先生になって・・・
クラスの子どもたちに対して感じる気持ちが、そうです。

まだ、若かった頃のことです。

僕は、他者への気持ちが、際限なく重たくなる理由がわかりませんでした。
例えようもなく、他者(クラスの子どもたち)への気持ちが重たくなるのを感じたある夜、郷里の父に電話して、その重さの理由を尋ねたことがありました。

どんなに心を尽くしても、こちらのしてほしくないことばかりする、クラスの子どもたちを『なんで心配してしまう』のか、その本当の理由が知りたかったのです。

郷里の父の部屋の机の上には、十字架があり、聖書と祈祷書が常にきちんと置かれていました。部屋の壁には作り付けの本棚があり、青年期に入った僕はそこから遠藤周作さんの「沈黙」や「海と毒薬」、八木重吉さんの「貧しき信徒」などを手にすることになります。父に連れられて教会へ行くことも、子ども心には不思議でしたが、僕の家では年中行事のひとつでした。父にとってイエス・キリストは、とても大切な存在であったようです。僕は、そんな家庭で育ちました。

その晩、電話の向こう側で、父は、ただ黙って、僕の話をきいていました。
なぜ、彼がずっと黙っていたのか、今はその理由がわかるような気がしますが・・・。
父がいつまでも黙っていることに耐えかねて、僕は彼にこう尋ねました。

「お父さんの信じる神なら、こんな時は何て言うの?」

「救い・・・って、何」

それまで黙っていた父が、即答しました。

「救いなんて、ない」

「それなら、なんなの。遠藤(周作)さんの『沈黙』と同じじゃないか。
 やっぱり、お父さんの神も黙ってるの?」

僕は、いちばん知りたかった疑問を重ねて彼にぶつけました。

「なんで、こんなに、赤の他人の人生が重いの?」

それから、父がゆっくりと、まるでひとりごとを言うかのように、答えてくれた言葉を、僕は今でも覚えています。それが、次の言葉でした。

「(私の名前)・・・な、ちんけな、安っぽい言葉に、聞こえるかもしれん。
 ちんけな、安っぽい言葉に、聞こえるかもしれんけど・・・」

「私は、それが愛だと思う。」

電話はそこで切れました。

内なる悲しみを分かち合うことは困難ですが、内なるこの愛は共有できます。

「女の一生 二部・サチ子の場合」 遠藤周作 著

青年期に読むべき、価値ある一冊だと思います。
初版は昭和61年ですが、現在でも重版されており、新しい本が入手できます。

ぜひ、手に取ってみてください。

花が咲いたよ

朝、仕事に行こうとして家の玄関を出たら・・・

スイセンの花が咲いていた。

僕は、大好きな、八木重吉さんの詩を思い出した・・・

 花

花はなぜ美しいか
ひとすじの気持ちで咲いているからだ

僕は、悲しみの塊のような人間だから、
どうしても過ちを繰り返してしまう・・・

たとえ、ひとすじの気持ちであっても・・・
それが自分にとって、どんなに美しいものであっても・・・
だれかを傷つけるなら、それは間違いだ。

空を見上げて、きみに心から謝りたいです。
ほんとうに、ほんとうに、ごめんなさい。

そう思ったら・・・
大好きな、八木さんの詩を、もうひとつ、思い出せた。

 ひかる人

私をぬぐうてしまい
そこのとこへひかるような人をたたせたい

ほんとうに、その通りだ。
それができたら、どんなにか、こころが明るく、軽く、なるだろう・・・

こんな くだらない僕を・・・
ひと思いに、ぬぐいさってしまえたら。

それが僕の ほんとう であるのに・・・
それが僕の 偽りなき ほんとう であるのに・・・

昨日の報道発表を聞いて
たくさんのひとが・・・僕に会いに来てくれて
美しい涙を流してくださった・・・

ぬぐいさってしまいたいような・・・こんな 僕のために。

わたしのまちがいだった
わたしのまちがいだった
こうして草にすわれば
それがわかる

あと何度、混乱を繰り返したら、美しい気持ちになれるだろう・・・
どれだけ思い悩んだら、この僕を卒業できるだろう・・・

どこに向かって歩けば、いちばん僕らしく、歩けるだろう

誰ひとり、傷つけずに・・・
誰ひとり、困らせずに・・・
静かに美しく咲いていた・・・

今朝、見た あのスイセンの花のように。

花が咲いたよ
花が咲いたよ
幼かった僕なら、きっと・・・笑顔で
そう、今の僕に言ったに違いないけど。

Begin

今日は、出張だった。

遅れてはならない大切な打ち合わせだったから、時間には十分余裕を持って出た。
昼を食べていなかったので、途中、コンビニでパンを買って・・・
僕は、なつかしい公園の駐車場にクルマを止めた。

最初から、そうしようと決めていたんだ。
数分間でもいい。思い切り、思い出に浸りたかった・・・。
新しい風景を見る、その前に。

(街路樹が・・・なくなってる?)

たしか、あそこには、ポプラ並木があった・・・はずだ。
僕だけじゃなくて、公園もいつしか年齢を重ねてた。

今の職場に6年・・・
その前の職場には4年・・・
この公園によく立ち寄ったのは、さらにその前の職場にいた頃だから、もう10年以上前のことだ。

あの頃、言葉にできないことがあると・・・
帰り道、ここでよく空を見上げた。
空は青かったこともあるし、茜色だったことも、星が瞬いていたこともあった。

樹があった場所に、10年の歳月と、今がある気がする・・・

(空港を出発したばかりの飛行機だ・・・)

そうだ・・・みんなを乗せて・・・ 遠い、とおい、ところへ・・・

優しかった微笑み
あの日の拍手
それから・・・ それから・・・

思い出せることすべてが泣きたくなるほどに、美しいのはなぜ・・・

思えば、胸いっぱいに広がるさみしさと、別れのかなしみを感じながら
大切に思えてならない多くのひとのしあわせを祈り続けてきた・・・たくさんの三月。

これが・・・ 僕の仕事なんだ。

今は、まだ、どうしても、うつむきたくなるけれど・・・
きっと思い出せるはずだ。

Begin

そう、Begin

美しい思い出とともに、また、きっと・・・歩き出せる。

Object Pascalと、僕と。

Conditional Compilation

条件付きコンパイル

Delphiからembeddable Pythonを利用するプログラムを書いた。これまでは32bit環境しか利用(作成)しなかったので、条件付きコンパイル(条件コンパイル)には縁がなかったが、64bit環境を利用しなければならない事情ができたので、初めてこれに挑戦。

結論だけ言うと、すごぉーく、カンタンだったー☆

【今回の記事】

1.ターゲットプラットフォームを切り替える
2.条件付きコンパイルやってみた
3.まとめ
4.お願いとお断り

1.ターゲットプラットフォームを切り替える

以前、スキャナーで読み込んで画像データ化した答案用紙の解答欄座標を、自動的に取得できるプログラムを書いた。もちろん、GUIはDelphiで作成。・・・とある事情から、コレを64bit化しなければならなくなった(「・・・とある事情」は次の記事以降のMyBlogに掲載)。

簡単に言うと、「手書きカタカナ文字の自動採点」をTensorFlowを使って実現しようとしたためです。正解率がどうがんばっても100%にならず、手書き答案の採点ソフトへの実装は見送りましたが・・・

このプログラムの核となる矩形(輪郭)検出部分は、OpenCVのfindContours関数を使い、座標取得部分は同じくOpenCVのboundingRect関数を使っている。OpenCV自体は、Delphi用ではなく、Python用のライブラリをembeddable Pythonにインストール。で、DelphiのObject PascalにPythonのScriptを埋め込み、必要な時にGUIの向こう側で走らせて、矩形検出を行っている。

32bitバージョンのembeddable Pythonを入れたフォルダの名称は「Python39-32」、64bitバージョンのembeddable Pythonを入れたフォルダの名称は「Python39-64」としてこのフォルダの名称以外はまったく同じコードでWin32版とWin64版の解答欄矩形検出プログラムとしてそれぞれコンパイル、同じくDelphiで書いた手書き答案採点プログラムから、採点準備の際に呼び出して利用している。フォルダ名の「39」だが、これはPythonのメジャーバージョンが「3」、マイナーバージョンが「9」であることを表しているのは言うまでもない。

Python環境を入れたフォルダの名称が異なるだけで、その他は32bit版も64bit版も一字一句まったく同じプログラム。これを別個のプロジェクトとして保存して、改良のための変更やバグの修正があった場合に、同じ内容を二度書くのはどう考えても効率が悪い。何かしら上手い方法はないのか・・・。

そこで、最も簡単にターゲットプラットフォームを切り替えるにはどうしたらいいかを調べたところ、条件コンパイル(条件付きコンパイル)という方法があることを知った。早速、見様見真似でやってみた☆

2.条件付きコンパイルやってみた

「Delphi ターゲットプラットフォーム 取得」をキーワードにして、Google先生に質問してみた。実は、この時点では、僕はまだ「条件付きコンパイル」なるものの存在を知らない。現在選択しているターゲットプラットフォームをプログラムコードから取得し、条件分岐でembeddable PythonへのPathを指定しようと思ったのだ。

  //手動でEmbeddable PythonへのPathを切り替え(存在の有無を調査)
  AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-32';
  //AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-64';
  if DirectoryExists(AppDataDir) then
  begin

  end;

Google先生が教えてくれたWebサイトへ行ってみると・・・

異なるバージョンの Delphi でソースを共有する

https://ht-deko.com/tech001.html

記事に『条件コンパイルを設定すればバージョンで異なるソースを記述できます。』と書かれている。

そんなの、あったんだー☆

で、今度は「Delphi 条件コンパイル」をキーワードにして再度、検索。新しいターゲットプラットフォームの追加方法と条件付きコンパイル(条件コンパイル)の記述方法を学ぶ。

ターゲットプラットフォーム(64ビットWindows)を追加するには、プロジェクトマネージャの「ターゲットプラットフォーム(Win32)」部分をポイントして右クリック、表示されたサブメニューの「プラットフォームの追加」をクリックすると次のWindowが表示されるので、追加したいターゲットプラットフォームを選択(されているけど)して、「OK」をクリックする。

「プラットフォームの選択」画面

すると、プロジェクトマネージャの表示は・・・

「Windows64ビット」が追加された!

条件付きコンパイルの記述方法は、処理を切り替えたいところで・・・

  {$IFDEF WIN32}
  //32bit環境での処理

  {$ELSE}
  //64bit環境での処理
  
  {$ENDIF}

・・・とすればいいだけ!とのこと。走召カンタン

$IFDEF に続く WIN32 部分を「シンボル」というらしい。もちろん、WIN32はターゲットプラットフォームが32ビット環境であることを意味する。他にもたくさんのシンボルが指定でき、例えばここを IOS とすれば、ターゲットプラットフォームが iOS であるかを判断できるとのこと。まぁ、僕がiOS用のプログラムを書くことはないだろうけれど・・・。

うれしい気持ちで、書いてみた!

うわー! 色が変わったぁー☆

どうやら、現在選択されていないプラットフォーム用のコードは色が薄くなるしくみらしい。DelphiのIDEの評価ってどうなのか、よく知らないけれど、コレはイイ!

ターゲットプラットフォームを切り替えると、コードの色も変わる

うれしくなって、何度も無意味に切り替えてしまう・・・

むかし、上野動物園でデカい白くまが「プールに落ちる」動作(飛び込みではない・単に落下するだけ!)を何度も何度も繰り返すのを見たけど、今、ようやく、あの時の白くまの気持ちがわかった気がする・・・。上野動物園、行きたいなー☆

うわー 壊れたー T_T

状況によっては、ターゲットプラットフォームを切り替えても、コードエディタの文字の色が切り替えに追随しなくなることがあるようだ。あわててDelphiのIDEを再起動。ターゲットプラットフォームを切り替えて直ったことを確認。よかったぁ・・・

これで別プロジェクトに分けなくても、プロジェクトマネージャーでターゲットプラットフォームを切り替えてコンパイルするだけで、32bit/64bitいずれの実行ファイルも生成できるようになった!

今回は、近所の小学生でもわかるような内容だったけれど・・・

すごくよくなれた気がする*(^_^)*♪

3.まとめ

(1)条件付きコンパイルを使えばターゲットプラットフォームの切り替えは簡単。
(2)コンパイラ指令は、コメント開始区切り記号{の後に $を先頭文字として記述。
(3){$IFDEF WIN32} //32bit環境での処理 {$ELSE} //64bit環境での処理 {$ENDIF}

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。