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

RAD Studio 12.0にPython4Delphiをインストールする!(その2)

前回、Delphi 12.0 に Python4Delphi をインストールする記事を書いた。

次は、その過去記事へのリンク。


実は、もっと、イイ方法が・・・☆☆☆

今まで知らなかったのですが、ウェルカムページの右上にある「GetItパッケージマネージャ」からインストールすれば、さらにカンタンでした!!

【もくじ】

1.エラーが消えない!
2.GetItパッケージマネージャからP4Dをインストール
3.まとめ
4.お願いとお断り

1.エラーが消えない!

いつからそうなったのか、わからないのだけれど、Delphi 11.2 & 12.0 で F9(実行)するとデバッグ出力にエラーがいくつか表示されるようになった・・・。

デバッグ出力:
clientcore\windows\advcore\ctf\uim\tim.cpp(800)\Msctf.dll!7577215A: (caller: 7576D910) LogHr(1) tid(1ff8) 8007029C アサーション エラーが発生しました。

プロセス XXX.exe (5568)

デバッグ出力:
clientcore\windows\advcore\ctf\uim\tim.cpp(800)\Msctf.dll!7577215A: (caller: 7576D910) LogHr(2) tid(1ff8) 8007029C アサーション エラーが発生しました。

プロセス XXX.exe (5568)

デバッグ出力:
onecore\internal\sdk\inc\wil\opensource/wil/winrt.h(1686)\Msctf.dll!757A7442: (caller: 757AF94D) Exception(1) tid(1ff8) 8007007E 指定されたモジュールが見つかりません。

プロセス XXX.exe (5568)

デバッグ出力:
clientcore\windows\advcore\ctf\shellhandwriting\client\handwritingclient.cpp(287)\Msctf.dll!757AFC28: (caller: 757A45FD) LogHr(3) tid(1ff8) 8007007E 指定されたモジュールが見つかりません。
    Msg:[onecore\internal\sdk\inc\wil\opensource/wil/winrt.h(1686)\Msctf.dll!757A7442: (caller: 757AF94D) Exception(1) tid(1ff8) 8007007E 指定されたモジュールが見つかりません。
] 

プロセス XXX.exe (5568)

「handwritingclient.cpp」ってなんだろ? ・・・と、Google先生にお伺いをたてても、関連する情報は何一つ得られず、「Msctf.dll」の方は検索結果にいろんな情報が出てくることは出てきても、なんかみんな的外れな感じで、走召!困った。

どうにもならないので、サポートセンターに援けを乞う。

サポートセンターの担当者の方と何度かメールでのやりとりを行った結果、Delphi 11.2 & 12.0 をどちらもアンインストールして、12.0 のみを再インストールすることに。

アンインストール & 再インストール作業は何のエラーもなく進行。

で、結果から言うと、F9(実行)で「エラーはそのまま表示され、消えません」でした。

T_T

エラーは消えなかったのですが・・・

ふと、思いついて update の有無を確認しに行った GetItパッケージマネージャで「愛しのP4D」を発見!

( こんなところにあったんだ。何で今まで気づかなかったんだろう? )

そう思いながら「インストール」ボタンを「ポチ」っとクリック。

結果・・・

今までの大苦労はなんだったんだ・・・
そう思うくらい、あっけなく
P4Dがインストールされました!

2.GetItパッケージマネージャからP4Dをインストール

転んでもタダでは起きない・・・とはまさにこのこと? 以下、GetItパッケージマネージャからP4Dをインストールする方法です。

Delphi 12.0 を起動すると表示される(デフォルト設定)、ウェルカムページ右上の

 ↑
これをクリック。

すると、次の画面が表示されるので、オプションボタンを図のようにクリック。

カテゴリ「Python」のオプションボタンをクリックすると、そこにP4Dがある。


ちなみに、著者の方のお名前の部分をクリックすると、GitHub の Python4Delphi のページに飛びます。

GitHub の Python4Delphi のページ

https://github.com/pyscripter/python4delphi


続けて、「インストール」をクリック。

「すべて同意する」をクリック。


インストールはすぐ完了。

カンタンなこと、この上なし!
七匹のヘビを無事発見!

3.まとめ

Delphi に Python4Delphi をインストールするなら、GetItパッケージマネージャからインストールするのが「走召」カンタンで便利!

さて、Delphi の「消えないエラー」をどうしよう・・・

4.お願いとお断り

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

VCLのMessageBox

Delphi 12.0を使い始めて、最初に気づいたことは、メッセージダイアログの異変だ。

( なんだか、シンプルになったなー )

Delphi 12.0 のメッセージダイアログ


そう。MessageDlgからアイコンが消えちゃった!

【もくじ】

1.Delphi 12.0 のメッセージダイアログ
2.MessageBoxがあった!
3.MessageBoxの使い方を学ぶ
4.MessageBoxの使用例
5.ダイアログからの戻り値
6.まとめ
7.お願いとお断り

1.Delphi 12.0 のメッセージダイアログ

今まで( Delphi 11.2 )なら・・・

i マークのアイコンがあるメッセージダイアログを表示するコード


実行すると・・・

表示されたメッセージダイアログ


System.UITypes を uses してないと・・・

「System.UITypesをusesしなさい」とアドバイスされる(これは 12.0 も同じ)。

Delphi 12.0 では・・・、System.UITypes は、もちろん uses して、

Delphi 12.0 でメッセージダイアログを表示


11.2 と同じコードを実行すると・・・

青地に白の i マークのアイコンが「ない」


それに、なんかエラーも。

僕には理解不能


( どうする? )

取り敢えず、Help でVCLのライブラリ・リファレンスを調べてみた。

キーワードを指定してEnterキーを押すと・・・


表示されたリファレンスの下の方に、次の一文を発見!

mtInformation じゃなくて、mtConfirmation だけど「Microsoftは、・・・削除してしまいました。」とある・・・


( これかー T_T )

mtInformation ではなく、mtConfirmation で試してみると・・・

引数を mtConfirmation に変更


実行すると・・・

アイコンは表示されないが、タイトルは「確認」に変わる


タイトルが「情報」から「確認」に変わるから、mtConfirmation は確かに効いてる。

「このアイコンがメッセージの具体的な種類を明確に表さない」って、んじゃもっと良いアイコンに変更するとか・・・、元々、アイコンはキャプションの補佐的役割を果たしているだけだから、それが「ない」よりは「あった」方が親切だと思うんだけれど・・・。

リファレンスによれば、第2引数に指定可能な値とその意味は次の通り。

Delphi 12.0 の Help の VCL のライブラリ・リファレンスを引用


リファレンスには「以前のダイアログ ボックスの概観を使用するには、Vcl.Dialogs ユニットの UseLatestCommonDialogs 変数を False に設定しなければなりません。」と但し書きがある。

以前の・・・って、XPの時代のことかな?・・・なんて思いながら、次のように指定して、

UseLatestCommonDialogs 変数を False に設定


実行すると・・・

さらにシンプルに・・・T_T


僕的には「シンプルじゃないのがよかった」ので、
とても「Simple is Best 」的な気持ちにはなれない!

あぁアイコンが欲しい!

もう、二度と・・・


きみに会えないかと思うと。

魂が折れそうなくらいの、さみしさがこみあげて・・・くる。

なんでだよー!

そう思いながら、ついでに mtError も試してみる。すると・・・

エラーのアイコンは表示される


「 × 」マークはMicrosoftさん的には、全世界共通「ダメです」って通じるってこと?


mtWarning もやってみた!

警告もアイコンは表示された


黄色の三角形に「i」が表示されたら、世界中の人が「警告」だって思うのかなー☆
Microsoftさん的には、「思う」ってことなんだろうなー。


ともあれ、これで確認できた。「情報」と「確認」がダメなんだ・・・。

でも、ない袖は振れない。

自分専用のダイアログを作るという、奥の手がないわけじゃないけど、それは最終手段。

MessageDlg のかわりになるものは・・・、ShowMessage 以外になんか、なかったっけ?

2.MessageBoxがあった!

ものごころついた時から・・・ってわけじゃないけど、僕はずっと MessageDlg関数を使ってきた。
ダイアログで使用する Font の大きさや色を変えたいなど、余程の理由がない限り、ユーザーに何かを伝えるいちばんの方法は、僕にとっては MessageDlg関数 で表示するメッセージだった。

それが ShowMessage ではない最大の理由は、テキストの他にアイコンも表示できるから。

だから、階層化テキストエディターの NaNaTree に書き溜めてきた、さまざまなメッセージの表示方法に関する覚書も、そのほとんどが MessageDlg関数 についてのもので、これにかわる代替手段など、これまで僕は考えたこともなかった。

( バージョンアップとかすると、毎回、いろんな落とし穴があるけど・・・ )

( 今回のも、これまで普通に使ってきたダイアログの仕様変更だもんなー )

( すーぱー困るけど、元に戻してくれるわけないし・・・ )

( なんか、代替手段、なかったっけ? )

そう思いながら、NaNaTree に保存した「メッセージ」に関する記録を下のほうへスクロールしてみる。

すると・・・

MessageBoxなる文字が・・・


あんまり使ったことのない、未整理の項目が下の方にあって、その中にMessageBoxという文字を発見。

( ほぼ使った記憶がないけれど、そんな関数もそう言えばあった・・・ )

さっそく、Helpを読んで、書き方を確認し、次のコードを実行してみた。

MessageBox関数を試す


はたして、アイコンは表示されるか?

祈るような気持ちで実行すると・・・

アイコン付きのダイアログが表示された!

何にも(追加で) uses しなくてイイし、いきなり書いて、すぐ動く!

コレだ! コレ!!

今度からコレで行こう☆

自分でもあきれるくらいの変わり身の早さ・・・

MessageDlg が泣いてるぞ。

愛してたんじゃなかったのかい・・・?

3.MessageBoxの使い方を学ぶ

VCL ライブラリの Help を読んでわかったことは、「MessageBox は、Windows API MessageBox 関数をカプセル化したものである」ということ。

このカプセル化で「具体的に何がどうなったか」と言うと、Windows.MessageBox とした場合には必要であったウィンドウ ハンドル パラメータが欠けていても、自動的に補完される、つまり、所有者ウィンドウへのハンドルは引数内で指定しなくてもよいということ。

ありがたき しあわせ。

ちなみに、Win32APIのリファレンス(C++)では、次のようになっているが、

int MessageBox(
  [in, optional] HWND    hWnd,
  [in, optional] LPCTSTR lpText,
  [in, optional] LPCTSTR lpCaption,
  [in]           UINT    uType
);

Delphi の Application.MessageBox のリファレンスでは、次のように

function MessageBox(const Text, Caption: PChar; Flags: Longint = MB_OK): Integer;

所有者ウィンドウへのハンドルが確かに省略されている。

実際のコードで、Windows.MessageBox とした場合には・・・

procedure TForm1.Button4Click(Sender: TObject);
begin
  Winapi.Windows.MessageBox(Handle, PChar('Do you know Delphi?'), PChar('情報'), MB_OK or MB_ICONINFORMATION);
end;

だったのが、第一引数のHandle は必要なくなり、( OK ボタンのみの表示でよければ)MB_OK も省略できるようなので、次のように

procedure TForm1.Button3Click(Sender: TObject);
begin
  Application.MessageBox(PChar('Do you know Delphi?'), PChar('情報'), MB_ICONINFORMATION);
end;

・・・とずい分、短くなる。それどころか、PChar型への型変換も省略可能なようで・・・

procedure TForm1.Button5Click(Sender: TObject);
begin
  Application.MessageBox('Do you know Delphi?', '情報', MB_ICONINFORMATION);
end;

型変換も内部で自動的に処理してくれるようだ(このまま実行可能。エラーも、警告も、ヒントも表示されない)。

Good! Gooder! Goodest!

これからは MessageBox で行こう!

4.MessageBoxの使用例

「 i 」マークのアイコンで、OK ボタンのみ表示する場合は、

Application.MessageBox(PChar('メッセージ'), PChar('情報'), MB_ICONINFORMATION);


警告マーク(黄)

Application.MessageBox(PChar('メッセージ'), PChar('警告'), MB_ICONWARNING);


禁止・エラー・停止マーク(赤地に白)

× の意味は、第3引数にSTOPとあるから「停止」が正しいのかな?

Application.MessageBox(PChar('メッセージ'), PChar('禁止'), MB_ICONSTOP);
Application.MessageBox(PChar('メッセージ'), PChar('エラー'), MB_ICONSTOP);


?マーク(青地に白)

Application.MessageBox(PChar('メッセージ'), PChar('質問'), MB_ICONQUESTION);

文字列型の変数を用意して、

var
  strMsg:string;
begin
  strMsg:='メッセージ';
  Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
end;


別の文字列型変数をさらに代入したり、また、改行を含む表示も、

procedure TForm1.Button2Click(Sender: TObject);
var
  strMsg, strPath:string;
begin
  strPath:='C:\abc\def';
  strMsg:='出力先は次の場所です。' + #13#10 + #13#10 + strPath;
  Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
end;

複数のボタンを表示。例えば、「はい」・「いいえ」の二択なら、

procedure TForm1.Button3Click(Sender: TObject);
begin
  //Information
  if Application.MessageBox(PChar('Do you know Delphi?'), PChar('情報'), MB_YESNO or MB_ICONINFORMATION) = mrYes then
  begin
    //[はい]が選ばれた時
    Application.MessageBox(PChar('Gooooooooooooood!'), PChar('情報'), MB_ICONINFORMATION);
  end else begin
    //[いいえ]が選ばれた時
    Application.MessageBox(PChar('No!'), PChar('情報'), MB_ICONINFORMATION);
  end;
end;
二択だから「キャンセルはない」
(閉じるボタンは自動的に無効になる)

ユーザーに「キャンセル」も許可するなら、

procedure TForm1.Button4Click(Sender: TObject);
var
  StrMsg: String;
  intRet: Integer;
begin
  StrMsg := 'Do you know Delphi?';
  intRet := Application.MessageBox(PChar(StrMsg), PChar('情報'),
                         MB_YESNOCANCEL or MB_ICONQUESTION);
  if intRet = mrYes then begin
    //[はい]を選択した時の処理

  end else
  if intRet = mrNo then begin
    //[いいえ]を選択した時の処理

  end else
  if intRet = mrCancel then begin
    //[キャンセル]を選択した時の処理
    Application.MessageBox(PChar('ユーザーによる処理のキャンセル'), PChar('情報'), MB_ICONINFORMATION);
  end;
end;
閉じるボタンは自動的に有効化されている


ちなみに「キャンセル」ボタンではなく、ダイアログ右上の「閉じる」ボタンをクリックすると・・・

閉じるボタンをクリックした場合は、キャンセル扱いになるようだ。

デフォルトでフォーカスを与えるボタンの指定方法は、第3引数の中で MB_DEFBUTTON3 のように、MB_DEFBUTTON の後ろにフォーカスを与えるボタンの番号を付けて指定する。番号はダイアログに表示するボタンの左から順番に1、2、3、4となるようだ。

第3引数に MB_YESNOCANCEL or MB_DEFBUTTON3 or MB_ICONQUESTION を指定すると、

左から三つめの「キャンセル」ボタンにフォーカスされた状態でダイアログが表示される


数字を付けない MB_DEFBUTTON や、数字を付けても MB_DEFBUTTON0(ゼロ)や MB_DEFBUTTON5 は未定義の識別子エラーになることから、指定可能なボタンの数は1~4の範囲内と決まっているようだ。

MB_DEFBUTTON5 は未定義の識別子エラーになる


「はい・いいえ・キャンセル」の三つボタンを表示する設定で MB_DEFBUTTON4 を指定しても未定義の識別子エラーにはならないし、実行時コンパイラはヒントも警告も表示しないが、フォーカスは最も左のボタンに当たるようだ。

intRet := Application.MessageBox(PChar(StrMsg), PChar('情報'),
                         MB_YESNOCANCEL or MB_DEFBUTTON4 or MB_ICONQUESTION);
MB_DEFBUTTON4 を指定してもエラーにはならない。
MB_DEFBUTTON4 を指定したにもかかわらず、
4つめのボタンがない場合には、最も左のボタンにフォーカスされる。

ってか、四つ目のボタンでナニ?

想像したけど、ちょっと思いつかない。表示できるボタンの種類を調べてみた。

MessageBox に表示できるボタンの種類は全部で七つあるようで、Flags パラメータで指定可能な値は、次の通り(Delphi 12.0 の VCL リファレンスより引用)

意味
MB_ABORTRETRYIGNORE メッセージ ボックスには、次の 3 つのボタンが配置されます: 中止、再試行、無視。
MB_OK メッセージ ボックスには、次のボタンが配置されます: OK。 これがデフォルトの設定です。
MB_OKCANCEL メッセージ ボックスには、次の 2 つのボタンが配置されます: OK、キャンセル。
MB_RETRYCANCEL メッセージ ボックスには、次の 2 つのボタンが配置されます: 再試行、キャンセル。
MB_YESNO メッセージ ボックスには、次の 2 つのボタンが配置されます: はい、いいえ。
MB_YESNOCANCEL メッセージ ボックスには、次の 3 つのボタンが配置されます: はい、いいえ、キャンセル。
Flags パラメータで指定可能な値

リファレンスには、さらに「これらの値は、希望する効果を得るため、組み合わせて使うこともできます。」とある。組み合わせるって、どういうこと?

ちなみに、MB_OK と MB_RETRYCANCEL を組み合わせてみると・・・

procedure TForm1.Button5Click(Sender: TObject);
begin
  Application.MessageBox('Do you know Delphi?', '情報', MB_OK or MB_RETRYCANCEL or MB_ICONINFORMATION);
end;

OK と 再試行、キャンセル 三つのボタンが表示されるのかなー? って思ったけど・・・

OK ボタンは表示されませんでした!


これは MB_OK と MB_ABORTRETRYIGNORE を組み合わせても同じで、MB_OK はやはり表示されない。MB_DEFBUTTON4 の役割は何なんだろう???

Web上の資料を調べてみた!

MessageBox 関数 (winuser.h)

https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-messagebox

上記リンク先の説明によれば MB_HELP もあるらしい。

Application.MessageBox('Do you know Delphi?', '情報', MB_OK or MB_HELP or MB_ICONINFORMATION);

実行してみると・・・

OK とヘルプは共存できるようだ

ただ、ヘルプをクリックしても、なんにも起きなかったが・・・
(処理を書いてないんだから、当然と言えば、当然)

しかし、戻り値の中に「ヘルプ」に相当する値がない・・・ような気が。上記の資料には「ユーザーが [ヘルプ ] ボタンをクリックするか F1 キーを押すと、 WM_HELPメッセージが 所有者に送信されます。」とあったけど、これ以上、追いかけると深みにはまりそうな気がするので、ヘルプに関する勉強は後日それが必要となった時にあらためてすることにして、ここではお茶を濁す。

結局、MB_DEFBUTTON4 の果たす役割はわからずじまい。残念!

それから、MessageBox関数には「すべてはい・すべていいえ」のボタンがないようだ。これもまぁ、それが必要になった時、考えることにする。僕が書くプログラムでそれが必要になることは、まず、ないだろう・・・。

あとわかったことは、「Text パラメータの値はメッセージで、必要なら 255 文字以上になっても構いません。 長文メッセージは、メッセージ ボックスでは自動的に改行されます。」ふむふむ、なるほど。

さらに「Caption パラメータの値はキャプションで、ダイアログボックスのタイトル バー上に表示されます。 Captions は 225 文字より長く指定できますが、改行されません。長文キャプションの場合、メッセージ ボックスの幅が広げられます。」とのこと。

5.ダイアログからの戻り値

ダイアログからの戻り値は、次の通り(Delphi 12.0 の VCL リファレンスより引用)

定数意味
mrNone0 なしユーザーが終了する前にデフォルト値として使用される
mrOkidOK ユーザーが[OK]ボタンで終了した
mrCancelidCancelユーザーが[キャンセル]ボタンで終了した
mrAbortidAbortユーザーが[中止]ボタンで終了した
mrRetryidRetryユーザーが[再試行]ボタンで終了した
mrIgnoreidIgnoreユーザーが[無視]ボタンで終了した
mrYesidYesユーザーが[はい]ボタンで終了した
mrNoidNoユーザーが[いいえ]ボタンで終了した
ダイアログからの戻り値

6.まとめ

プログラムからユーザーに対してメッセージを表示する方法は、他にもいろいろあるみたいだけれど、MessageBox関数最大の強みは「すべての OS で利用可能」なこと。

OK と、はい・いいえ、キャンセル の各ボタンが利用できれば十分というのであれば、アイコンが表示されなくなった MessageDlg関数のかわりに MessageBox関数が使える。もちろん、ダイアログにアイコンも表示される。

ただ、これまでに書いてきたプログラムを Delphi 12.0 で修正・更新する際にはMessageDlg関数を、MessageBox関数に変更するという、地道な作業が待ってることだけがちょっと気になるが。

まっ ヒマだから、いいか!

7.お願いとお断り

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

RAD Studio 12.0にPython4Delphiをインストールする!

追記(20231208)

さらにカンタンな方法がありました!

https://coding-tips-memoranda.com/rad-studio-12-0%e3%81%abpython4delphi%e3%82%92%e3%82%a4%e3%83%b3%e3%82%b9%e3%83%88%e3%83%bc%e3%83%ab%e3%81%99%e3%82%8b%ef%bc%81%ef%bc%88%e3%81%9d%e3%81%ae%ef%bc%92%ef%bc%89/

以下、苦労を伴うインストール方法の記録です(お読みいただく価値のない情報です)。 T_T

2023年11月8日、RAD Studio 12.0(僕にとってはDelphi 12.0)がリリースされた(ようです)。
アップデート・サブスクリプションの支払いを終え(個人で購入しているのは僕くらいだろうが・・・)、届いたメールの製品アップデートリンクをクリックして、最新の更新をチェックしたら、12.0が!

(誰も教えてくれないから、リリースされたこと自体、まったく知りませんでした! T_T )

そういえば・・・RAD Studioのメジャーアップデートは毎年この時期だったような。

あわわわわわわわわわわ ひー!ひー!(驚愕的感動を表現)

早速、Web Installを実行。

Delphi 12.0 のインストールは何の問題もなく、15分くらいで終了(XEの頃は時間がかかったけど)。

11.2 が入っている環境にインストールしたためか(?)、シリアルナンバーの入力なども一切ありませんでした! カンタン。気持ちいい。

続けて、Python4Delphiも最新版(RAD Studio 12.0対応版)をインストール。

以下、その時のメモです!

【もくじ】

1.Python4Delphiの最新版をダウンロードして展開する
2.フォルダ構成を整える
3.Python4Delphiの最新版(RAD Studio 12.0対応版)をインストール
4.ライブラリパスを確認
5.まとめ
6.お願いとお断り

1.Python4Delphiの最新版をダウンロードして展開する

まず最初に、Python for Delphi(P4D)をGitHubから入手してDelphiにインストール。

P4Dの入手先URL https://github.com/pyscripter/python4delphi

Codeをクリックすると表示されるサブメニューのいちばん下にDownLoad ZIPがあるので、これをクリックしてZIPファイルをダウンロードし、任意の場所(フォルダ)に解凍する(ここではダウンロードするフォルダの名前を「P4D」として説明)。

Download ZIPをクリックして最新版を入手する

ダウンロードが完了したら、ダウンロード先フォルダにはコレがあるはず。

python4delphi-master.zipを任意の場所に「P4D」フォルダを作成して、そこへコピペする

P4D」フォルダを作成するのは、できればあまり階層の深くない、絶対に忘れない場所がよいと思います。理由は、後からそこにライブラリパスを通すから。バックアップなど取る時にも、忘れないような場所に作成してください。

zipファイルを右クリックして、表示されるサブメニューの「すべて展開」をクリック。

zipファイルを展開(解凍)

そのまま P4D フォルダの直下に展開(解凍)する。

Pathは敢えていじらずに、そのまま「展開」をクリック

展開(解凍)が完了すると、P4D フォルダの下に「python4delphi-master」フォルダができ、その下に同じ名前でもうひとつ「python4delphi-master」フォルダができる

この中に7匹のヘビがいる。はやく会いたい。

2.フォルダ構成を整える

この時点でフォルダ構成は・・・ちょっとややこしいが、次のようになっている(はず)。

¥任意の場所¥P4Dpython4delphi-masterpython4delphi-master

とりあえず、いちばん下の python4delphi-master フォルダをダブルクリックして開き、中にあるものすべてを CTRL+A で全選択して、CTRL+X で切り取り、ひとつ上の階層の python4delphi-master フォルダ内に CTRL+V(貼り付け)する。

で、いちばん下の階層の python4delphi-master フォルダは不要なので消去(削除)する。

さらに、上の階層の python4delphi-master フォルダの名前を手動で「P4D」に変更(リネーム)する。

これでフォルダ構成は、次のようになる。

¥任意の場所¥P4DP4D

いちばん下の P4D フォルダをダブルクリックして開くと・・・

Install フォルダ内にある「README.md」に、実は重要な情報が書かれている

【README.md】※ 原文のまま

## P4D Installation using [MultiInstaller](https://github.com/pyscripter/MultiInstaller)

Use for Delphi Seattle (10.4) or later to install all packages in one step. 

1. Clone or copy the Python4Delphi git repository to a folder of your choice.  **The setup.ini file assumes that the folder is called "P4D"**.  If you chose to name your folder differently then modify the "Folder" option in setup.ini.
2. Close all Delphi IDEs running.
3. Run MultiInstaller.exe
4. Select the packages you want and press Next
5. In the dialog box specify the _**parent folder**_ of "P4D" (i.e. the folder containing the directory to which you have copied Python4Delphi) and the Delphi target version.  Then press Next to install the components

Google先生、曰く・・・(文字に色付けしたのは僕です)

## [MultiInstaller]を使用したP4Dのインストール(https://github.com/pyscripter/MultiInstaller)

Delphi Seattle (10.4) 以降の場合は、すべてのパッケージを 1 ステップでインストールするために使用します。

1. Python4Delphi git リポジトリを選択したフォルダーにクローンまたはコピーします。 **setup.ini ファイルでは、フォルダーの名前が「P4D」であると想定しています**。フォルダーに別の名前を付けることを選択した場合は、setup.ini の「フォルダー」オプションを変更します。
2. 実行中のすべての Delphi IDE を閉じます。※ コレも重要!な注意点のひとつかと・・・
3. MultiInstaller.exe を実行します。
4. 必要なパッケージを選択し、「次へ」を押します
5. ダイアログ ボックスで、「P4D」の _**親フォルダー**_ (つまり、Python4Delphi をコピーしたディレクトリを含むフォルダー) と Delphi ターゲット バージョンを指定します。次に、「次へ」を押してコンポーネントをインストールします

僕なりの解釈は(間違ってるカモだけど)・・・

Python4Delphi をコピーしたフォルダ名は「P4D」であり(であることを想定しており)、
さらに、インストール時に表示されるダイアログボックスでは・・・

P4D」の _**親フォルダー**_ を指定

つまり、「その親フォルダ(階層がいちばん上の P4D )を指定せよ」

と言っている・・・。

フォルダ構成を README.md の指示通りに整えたところで、

¥任意の場所¥P4DP4D¥Install フォルダを開き、

そこにある MultiInstaller.exe をダブルクリックして実行する。

Install フォルダにある MultiInstaller.exe をダブルクリック

ちなみに、拡張子md は、「Web 用のドキュメントの作成によく使用される、読み書きしやすいように設計されたプレーンテキスト」に使う拡張子だそう。

ちなみに「プレーンテキスト」は、「文字だけで構成され、レイアウト情報や装飾情報などを持たないデータのこと」だそうで。

勉強になりますー。

3.Python4Delphiの最新版(RAD Studio 12.0対応版)をインストール

こうしてインストール前の最大の難関?を乗り越え、早速、Python4Delphi をインストール。

Install フォルダにある MultiInstaller.exe をダブルクリック(再掲)

次の画面が表示される。

フォルダの選択ダイアログ

Select Destination directory to install all the component packages. ・・・

こちらもGoogle先生曰く、

「すべてのコンポーネント パッケージをインストールするには、宛先ディレクトリを選択します。」

どうも、この、「宛先」という訳がピンとこないけど・・・。

まぁ、「宛先」は「参照元」に読み替えて・・・。

それが、先ほどの「README.md」に書かれていた「Python4Delphi をコピーしたディレクトリを含むフォルダー」・・・つまり、「P4D」フォルダなんだろうな・・・ みたいな・・・

ってか、もっと正直に言うと・・・、RAD Studio 12.0 をインストールしたから、唯一、僕が必要とするサードパーティー製コンポーネント Python4Delphi も入れなきゃって思って、前回の(11.2 への)インストール作業後、大切に保存しておいた P4D¥Installフォルダ内の MultiInstaller.exe を起動したら・・・

RAD Studio 12 Athens がインストール先の候補として出てこない!

つまり、この MultiInstaller.exe は RAD Studio 12 Athens のインストールパスを拾って・・・「ない」。

このダイアログを見たとき、一瞬、(もうダメだ・・・)と思ったのですが、その直後、このインストーラー自体が1年前のものだったことを思い出し、・・・だとすれば、RAD Studio 12 Athens が表示されなくて、むしろ当然・・・。ここで初めて Python4Delphi も最新版が必要だと気づき・・・

さらに、オプションボタンがこのダイアログに「1つしかない」意味まで見えた気が・・・

(オプションボタンだから、インストール対象としてパスを通すのは、1バージョンに限定ってことなんだろうけれど・・・)

(それよりも・・・、ダイアログのCompile packages ~の余白が有り余ってるのは、RAD Studio のメジャーバージョンアップを見越して、後からボタンを追加できるよう、予め余裕を持って設計したから?)

(・・・もし、そうなら12.0対応版があるに違いない。いや、きっとある!)

あわてて GitHub へ行って12.0 対応版の有無を確かめたというのが事の真相。

思った通り、GitHub の Python4Delphi は、12.0のリリースに合わせて最新版にアップデートされてました・・・。作者の方に心から感謝!

Go To 「3.Python4Delphiの最新版(RAD Studio 12.0対応版)をインストール

これで無限Loop に。Blogまでスパゲッティ化しちゃった・・・。

↑ コレは古い時代のプログラマーにしか、通じない言葉かな?

取り敢えず、無限Loopはなんとかして乗り越えたコトにして・・・

宇宙のはじまりだって、トンネル効果が起きた時、虚数時間が流れていて、上り坂が下り坂になった・・・みたいな話を、聴いたような・・・。聴かなかったような。で、宇宙って、通れないはずの壁から果たして沁み出すものなんだろーか。

ハイゼンベルクさんは、連合軍の科学者たちのことを、どう思っていたんだろう・・・

RAD Studio 12.0 対応版のP4D付属 MultiInstaller.exe を起動して表示されるフォルダの選択ダイアログは、前掲の通り。

Browseボタンをクリックして・・・

Compile packages ~の欄には RAD Studio 12 Athens が増えましたが、欄の上下に「まだまだ余裕」があります。これを見て、先ほどの予感は大きく自信を得て・・・「これはつまり、今後数十年以上先までDelphiのメジャーバージョンアップが続々と行われることを見通して、必要十分と思われる余白を予め用意した先見の明溢れる非常に大胆な先進的設計である」という確信に変わりました。

是非、そうであって欲しい・・・と、心から願っています!

ダイアログがこのままの大きさでも、

1、2、3・・・と、近未来、確実にそこに入るであろうオプションボタン位置を予想してみると、Delphi のメジャーバージョンアップにあと10回は余裕で対応できそうです*(^_^)*♪

いいぞ。さすが、P4D!

こういう応援の仕方もあったのか・・・

こんどから、

僕も真似しよー!!

解凍先フォルダの階層Topにある P4D フォルダを指定
Compile packages and install on IDEにチェックして、RAD Studio 12 Athensを選択

あとは Next ボタンをクリックしてインストーラーにすべておまかせで、P4Dをインストールするだけ。
無事完了すれば、次のようなダイアログが表示される(画像は前バージョンのもの)。

作業の記録をとり忘れたので、これは 11.2 に P4D をインストールしたときの画像

最後に Finish をクリックしてインストール作業終了。

Delphi 12.0 のIDEを起動して、パレットを確認。

7匹のヘビを無事発見。

4.ライブラリパスを確認

Delphi のIDEを起動し、「ツール」→「オプション」→「言語」→「Delphi」→「ライブラリ」の順にクリックして下の画面を表示。

プラットフォームを選択して、ライブラリパスの「…」ボタンをクリックする

ライブラリパスの一覧が表示されるので、そのいちばん下に P4D へのパスの設定があることを確認する(パスはインストール時に自動で設定されるようだ)。

Library パスの一覧の下から3つが P4D へのパス(自動で設定される)

上の画面では 「Windows 32ビット」 のプラットフォームに対する設定を確認している。念のため、「Windows 64ビット」 のプラットフォームに対しての設定も確認する。

プログラムのコンパイルを実行すると、Delphiはいちばん最初にプロジェクトファイル(.dproj)のあるフォルダ(ここはパスが通っているから登録は不要)を検索し、必要なユニットファイル等の有無を確認。もし、そこに必要なファイルがなければ、この画面に登録したライブラリパスを検索するようだ。

5.まとめ

(1)RAD Studio 12.0 のリリースに合わせ、Python4Delphi もアップデートされていた。
(2)Python4Delphi のインストールは専用の「MultiInstaller.exe」で実行する。
(3)Python4Delphi のデータは「¥任意のフォルダ¥P4D¥P4D」フォルダ内に置く。
(4)インストール後、念のため、ライブラリへのパスが設定されていることを確認する。

6.お願いとお断り

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

浮動小数点

学生の頃から、2進数が苦手だった・・・。
浮動小数点も、本当のところはよくわからなかった・・・。

それがわからなくても、日常生活で苦労することは皆無だったし、
PCに触れている時でも、これと言って計算に困るような出来事もなかった。

そう、これまでは・・・

ただ、職場が変わって、状況が変化。
苦手だった2進数や、浮動小数点を、ほっとけなくなっちゃった・・・。

もくじ

1.10進数を2進数に変換
2.浮動小数点
3.0.1(10)を2進数に変換
4.まとめ
5.お願いとお断り

1.10進数を2進数に変換

これが苦手で、小テストとかあって、本当に困ってるひとが、もし、いたら、
僕なりに苦しい中から見出した絶対忘れない変換方法をお伝え出来たらと思って、
この記事を書いています。

まず、僕は、よく見るコレが『なぜか苦手』でした(今でも)。
例えば7(10)を2進数に変換する場合、

10進数を2進数に変換(その1)

答えは、図の矢印の順に読んで、0111(2) 。だけど・・・
この書き方。すごく、思い出しにくい。
特に焦ったりすると、何か、どこかで混乱して、僕は必ず間違えてしまう。

自分なりに考えて出した「結論」は・・・この書き方、普段ほとんど使わないから・・・かな? みたいな

そこで、小学校以来慣れ親しんだ書き方がよかろうと思い、
自分では、より『素直』に思えるカタチに書き方を変更。
結果、これなら絶対間違えないと思えました。
自分的には、この書き方の方が、なぜかとても安心感があります。なんでかな?

10進数を2進数に変換(その2)

自分に対して、自信が持てない本当の理由は、わかってます。
答えを出す方法だけが知りたくて、『なんで2で割り算するのか?』考えたことがなかったからです。
つまり、『ほんとうのこと』から、僕は目を反らし続けてきた・・・から。

理由はいくらでもあげられます。

めんどくさかったから
考えたくなかったから
試験に通ればそれでよかったし
そんなこと、どうでもよかったカラ

でも、とうとう、ここで、それは通用しなくなりました。
『2で割り算する』、その理由を僕は説明しなければならない。

誰もがわかるように・・・。

とりあえず、わかりやすい10進数で考えてみます。
10進数の基数は「10」、では基数で割り算するってどういうことなのか?

基数で割り算した余りが、元の10進数の一の位、十の位、百の位、千の位と一致します。
つまり、各「位(くらい)」の数を求めていたわけですね!

2進数の場合、基数は「2」ですから、基数で割り算すれば、余りは「0」か、「1」のいずれかになります。これが、その桁の2が「あるか・ないか」を教えてくれるわけです。

例えば、10進数の「123」を2進数に変換する場合、


01111011(2)の0と1はそれぞれ、その桁の2が「あるか・ないか」だから

電卓の種類を「プログラマー」に変更して、ラクして確認。

こんなイイモノがあったんだ!

手計算でも確認(これが16進数で桁数が多かったら、挫けそうだなー)。

2進数でも、余りは、そのまま、各位(くらい)の数になりました。
よかった。よかった。

2.浮動小数点

数値のどこまでが信頼できる桁であるのかを表すのに、有効数字を用いますが、例えば


有効数字の 1.234 の部分を「仮数(かすう)」というそうです。

2進法では、次のような場合を考えると、


2倍すると1桁、位が上がりますから、2の2乗倍=4倍すれば2桁、位(くらい)が上がることになります。これを小数点の位置で言えば、2をかけるたびに、小数点の位置が右へ移動するわけです。

(だから「浮動小数点」って言うのかぁ・・・)

ここでの仮数は「1.0111」ですが、実は仮数には重要な決まりがあって、「整数部分を1桁とし、そこに0以外のいちばん上の位の数を置く」のだそうです(⇨ IEEE754 という方式に準拠した場合?)。

・・・と、いうことは、2進数なら「0」と「1」しかないから、仮数の整数部分は必ず「1」になることになります。

64ビットの浮動小数点数(2進数の場合)では、符号ビットは1ビット、指数部は11ビット、仮数部は52ビットで表わされることが多いそうで、符号は+とーのどちらかだから1ビットでOKとして、仮数部ではその整数部分「1」を省略してしまうとのこと。なんで?

仮数部で整数部分の「1」を省略する理由がわからなかったので調べてみました。結果を知って納得。

整数部分の1を省略すれば、仮数部を1ビット増やすことができるので、浮動小数点形式の精度を向上させることができる。

(すごーい!)

仮数の整数部分を必ず「1」とすることで、そんなことも可能になるわけですね!

3.0.1(10)を2進数に変換

すーぱー 苦しんだのが、コレです。

10進数の小数を2進数に変換するなんて、もう長いことやってない。
やったことがあるとしても、15年以上前です。その記憶の欠片すら、残ってません。

表し方を調べてみると・・・、整数でやったコレを、そのまま2のマイナス1乗みたく、右側へ拡張して小数点以下を表現するとのこと。


そー言えば、はるか、むかし。
なんで2のゼロ乗が1になるのか、さっぱりわからず、悩んだことがあったよーな・・・。
わかってみれば、カンタンだったけど。


この関係を含めて10進数の小数を位(くらい)の数として表せば、


2進数でも、考え方は同じ。
違うのは、それがいくつあるか? ではなく、単に「ある」か・「ない」か が、「ある」=「1」、「ない」=0で示されること。あとあと、これがかなり重要な理解のポイントになります!


特に、0.X とした場合の小数点以下の部分 は(1の位が0であることを明示して表せば)、


この関係がそのまま使えれば、話はカンタン。
例えば10進数の 0.625 を2進数に変換したい場合、0.625 が 0.5 と 0.125の和であることに気付けば、


んじゃ、10進数の 0.1 を2進数に変換するには?
理解のために、全桁「1」で数値が「ある」ものとし、位(くらい)の数を書き出して考えます・・・


2進数で計算すると、このような誤差が必ず生まれてしまう!

だから、0.1 に少しずつ近づくように(0.1 を超えないように)、足し算可能な、よりちいさな数を求め、どこまでもこれを繰り返して(=循環する理由)行くわけですか・・・

(それはわかったけど、0.1(10) を2進数に変換するわかりやすい方法は・・・)

Google先生に、いろいろたずねても、コレだぁ!・・・みたいな答えは教えてくれない!!

それでも、いろんなWebサイトさんの断片的な情報を集めて、ようやく変換方法だけはわかりました☆

それが、こちら(10進数の 0.1 を2進数に変換)

基数で割り算でなく、基数を「掛け算」!


なんで、これで変換できるのか?
次の式を書き出して、ひたすら考えます・・・(この場合、左辺のxは もちろん 0.1)。


10進数の 0.1 を2進数に変換するとき、なんで割り算でなく、基数を掛け算してるのか? まず、それがさっぱりわからないけれど、ここでまず思い出したことは、これは2進数表現だから、an は必ず0か1のいずれかになるということ。

つまり、an が「1」ならその部分は数値が「あり」で、an が「0」なら「ない」わけです。

一歩、前進。

問題は基数を「掛け算」する理由。

基数を掛け算する・・・、つまり、2倍するってことは、意味的にはナニをしているのかというと、2倍して1桁、位を上げていることになる。おそらく、この部分に重大な意味があるような気が・・・。

てか、ナンで桁を上げる必要があるのか?

それがアタマにこびりついて、離れません。

(あぁ ナニを見ても0と1に見える・・・)

誰か、教えてくれないかなー

ブラウザのタブを切り替えながら、既に何度も眺めたWeb上の情報にもう一度目を通します。

(なにか、見落としてること、あるんじゃないかなー)

あるWebサイトにあった次の文がなぜかキラキラ輝いて見えます。でも、なんで輝いて見えるのか、それがわかりません。

2進数で表された1.0と、10進数で表された1.0は、数の重みが等しい(同じ数!)

なんか、ものすごく大切なことを伝えてくれている気がするのですが、その「ものすごく大切なこと」がナンなのか? それがどうしてもわかりません!

0.1 かぁ・・・ これは10進数の小数で・・・

それを2進数にしたいんだよなぁ・・・


左辺の 0.1 は、10進数の領域にあって・・・、右辺は、2進数の領域にあると考えれば・・・

右辺のan は絶対に「1」か「0」のどちらかになる(2進数だから)。

「1」なら「数値あり」で、「0」なら「ない」。

で・・・

1 は2倍すれば、一の位に現れる・・・

2 は22=4倍すれば、一の位に現れる・・・

3 は23=8倍すれば、一の位に現れる・・・

2進数も、10進数も、一の位だけは・・・1が意味合い的に同じ「ひとつだけあるの1(イチ)」。

・・・って、コトは、

二つの異なる領域をイコールで結ぶために必要な、その条件が一の位・・・

そうか、一の位じゃないと・・・「0」か、「1」かが、きっと 見えない んだ!

だから、どんどん2倍して、
1、a2、a3・・・が、次々に一の位になるように、桁を上げるんだ☆

一の位になって初めて、それが「0」なのか、「1」なのかが 見える んだ!!

あー☆ わかったカモー!!

だから、(上の計算で)基数を掛け算した結果、整数部に「1」が出てきた時は、それが「ある」ことさえわかればイイから、その存在を無視して、小数部だけを基数倍(2倍)してるんだ!

コレ、考えた人、天才だ☆

4.まとめ

今回は内容が多岐にわたり、コレが最も適切かと・・・思われます。

5.お願いとお断り

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

FireMonkeyのMessageDialog

FMX事始め

1.FMXでMessageDialogを表示する
2.TMsgDlgTypeに指定できる値はVCLと同じ
3.表示できるボタン
4.表示できないボタン
5.ダイアログ右上の閉じるボタンの挙動
6.まとめ
7.お願いとお断り

1.FMXでMessageDialogを表示する

いろいろな事情からFMXプラットフォームで、あるプログラムを書くことになった。使い慣れたVCLと違って、FireMonkeyはずっと以前に一度だけWAVファイルの再生プログラムを作った時触れたことがあるだけで、まともに触るのは今回が初めて。

最初にいちばん困ったのはユーザーへのメッセージの出し方。なんでかわからないけれど、普通にShowMessageすると、その直後、FMXプラットフォームでは、結構な頻度でエラーが発生する気が・・・。

だから、最初に書いたデータベース接続のプログラムは、極力ShowMessageを使わない方向で書いたんだけど、2作目のテキスト入力練習プログラムではそうも行かず、良い機会だと思ってShowMessageより見た目が華やかなMessageDialogの正しい使い方を調べてみた。

思った以上に情報が少ない気がしたので、学んだことを備忘録として、まとめておく。

まずは、iマーク付きのMessageDialogの出し方。

implementation

uses
  FMX.Platform, FMX.DialogService;

{$R *.fmx}

procedure TForm1.Button1Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  //mtConfirmationだとBeep音が鳴らないが、mtInformationだとBeep音が鳴る
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync,
    IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], TMsgDlgBtn.mbOK, 0,
      procedure(const AResult: TModalResult)
      begin
        if AResult = mrOK then
        begin

        end;
      end);
  end;
end;

実行すると・・・

ここにたどり着くまで、結構長かった・・・
ほんとに、ようやくって感じ。

調べてわかったことは・・・

var
  ASyncService:IFMXDialogServiceASync;

・・・と宣言するためには、

uses
  FMX.Platform;

uses に FMX.Platform が必要で、さらに、サポートの有無を調査するif文の・・・

if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync,
    IInterface(ASyncService)) then

TPlatformServices も FMX.Platform を参照している。

で、本命の MessageDialog を表示するには、さらに・・・

uses
  FMX.Platform, FMX.DialogService;

uses に FMX.DialogService も追加しなければならない。

2.TMsgDlgTypeに指定できる値はVCLと同じ

上のユーザーへの情報提供(Info)に加えて、ユーザーに確認する場合は、

TMsgDlgType.mtConfirmation

ユーザーに警告。

TMsgDlgType.mtWarning

ユーザーにエラーを報告。

TMsgDlgType.mtError

mtCustomってのもあったけど・・・

TMsgDlgType.mtCustom
画像は、何も出てこなかった・・・。
キャプションもProject1(アプリケーション名)になってる・・・。

実質、「情報提供・確認・警告・エラー」の4つ型があり、これはVCLと変わらない。

3.表示できるボタン

ユーザーの応答が「OKボタン押し下げのみ」であれば、MessageDialogの最後の引数を別手続きにして、それを呼び出す形にすればいいのかと・・・

  private
    { private 宣言 }
    procedure MsgDlgProc(const AResult: TModalResult);

として、Shift+Ctrl+Cで手続きを作成。

procedure TForm1.MsgDlgProc(const AResult: TModalResult);
begin
  //何もしない手続き

end;

応答が「OK」のみの場合は、これを呼び出し。

procedure TForm1.Button2Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, 
      [TMsgDlgBtn.mbOK], TMsgDlgBtn.mbOK, 0, MsgDlgProc);
  end;
end;

コードが短くなって、なんとなくすっきりした。

でも、「はい」・「いいえ」・「キャンセル」のようにボタンを複数表示するとそうもいかない。

procedure TForm1.Button3Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbYes, 0,
      procedure(const AResult: TModalResult)
      begin
        if AResult = mrYes then
        begin
          ShowMessage('Goooooooood!');
        end;
        if AResult = mrNo then
        begin
          ShowMessage('No Good!');
        end;
        if AResult = mrCancel then
        begin
          ShowMessage('Cancel');
        end;
      end);
  end;
end;

case文でもよいようだ。

procedure TForm1.Button3Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbYes, 0,
      procedure(const AResult: TModalResult)
      begin
        case AResult of
          mrYes:ShowMessage('Goooooooood!');
          mrNo:ShowMessage('No Good!');
          mrCancel:ShowMessage('Cancel');
        end;
      end);
  end;
end;

caseのリストが表す値は、case文内で一意、部分範囲またはリストの重複がなければ昇順とかリストの並びは関係ないようだ。また、このようによく使用されるボタン値は、セットになった定数として用意されていて、例えば上の場合は次のように指定できる。

TMsgDlgType.mtInformation, mbYesNoCancel, TMsgDlgBtn.mbYes, 0,
こっちの方がカンタン!

embarcaderoさんのWebサイトでは、TMsgDlgBtnは種類がたくさん紹介されていて、

定数意味
mrNone0結果なし。ユーザーがフォームを終了するまでのデフォルト値として使用されます。
mrOkidOK = 1ユーザーは[OK]ボタンでフォームを終了しました。
mrCancelidCancel = 2ユーザーは[キャンセル]ボタンでフォームを終了しました。
mrAbortidAbort = 3ユーザーは[中止]ボタンでフォームを終了しました。
mrRetryidRetry = 4ユーザーは[再試行]ボタンでフォームを終了しました。
mrIgnoreidIgnore = 5ユーザーは[無視]ボタンでフォームを終了しました。
mrYesidYes = 6ユーザーは[はい]ボタンでフォームを終了しました。
mrNoidNo = 7ユーザーは[いいえ]ボタンでフォームを終了しました。
mrCloseidClose = 8ユーザーは[閉じる]ボタンでフォームを終了しました。
mrHelpidHelp = 9ユーザーは[ヘルプ]ボタンでフォームを終了しました。
mrTryAgainidTryAgain = 10ユーザーは[やり直し]ボタンでフォームを終了しました。
mrContinueidContinue = 11ユーザーは[続行]ボタンでフォームを終了しました。
mrAllmrContinue + 1(12 つまり $C)ユーザーは[すべて]ボタンでフォームを終了しました。
mrNoToAllmrAll + 1(13 つまり $D)ユーザーは[すべていいえ]ボタンでフォームを終了しました。
mrYesToAllmrNoToAll + 1(14 つまり $E)ユーザーは[すべてはい]ボタンでフォームを終了しました。
https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.StdCtrls.TCustomButton.ModalResultより引用

さらに、セットになった定数が5つあるとのこと。

定数意味
mbYesNoCancelmbYes、mbNo、および mbCancel
mbYesAllNoAllCancelmbYes、mbYesToAll、mbNo、mbNoToAll、および mbCancel
mbOKCancelmbOK および mbCancel
mbAbortRetryIgnorembAbort、mbRetry、および mbIgnore
mbAbortIgnorembAbort、mbIgnore
https://docwiki.embarcadero.com/Libraries/Alexandria/ja/Vcl.Dialogs.TMsgDlgBtnより引用

4.表示できないボタン

僕のPCだけ、そうなのかもしれないけど。中には表示できないボタンが・・・。例えば、

procedure TForm1.Button7Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation,[TMsgDlgBtn.mbRetry],TMsgDlgBtn.mbRetry,0,
      procedure(const AResult: TModalResult)
      begin
        case AResult of
          mrOK:ShowMessage('OK!:了解');
          mrCancel:ShowMessage('Cancel:取消');
          mrAbort:ShowMessage('Abort:中止');
          mrRetry:ShowMessage('Retry:再試行');
          mrIgnore:ShowMessage('Ignore:無視');
          mrYes:ShowMessage('Yes:はい');
          mrNo:ShowMessage('No:いいえ');
          mrClose:ShowMessage('Close:閉じる');
          mrHelp:ShowMessage('Help:要援助');
          mrAll:ShowMessage('All:すべて');
          mrNoToAll:ShowMessage('NoToAll:すべていいえ');
          mrYesToAll:ShowMessage('YesToAll:すべてはい');
        else
          //ないと思うけど、
          ShowMessage(IntToStr(AResult));
        end;
      end);
  end;
end;

ボタンに mbRetry を指定しても、上の手続きを実行すると表示されたダイアログは・・・

普通のOKボタン!

で、OKを押し下げ。・・・ると

なんでかなー?

でも、次のように指定すると、

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbCancel,TMsgDlgBtn.mbRetry]
指定した順番と並びが逆だけど、「再試行」ボタンが表示された!

で、「再試行(R)」を押し下げ。・・・ると

表示できる場合とできない場合があるらしい。つまり、これはダイアログに表示可能なボタンの設定(組み合わせ)を FMX の MessageDialog は内部的に持っているということ? それから、キャンセルボタンは必ず右側へ設置される?・・・から、ボタンの表示される順番もまた、決まっているという理解でいいのかな?・・・みたいな。

他にも、mbAbortRetryIgnore を指定して、デフォルトで選択状態にするボタンに mbRetry を指定しても・・・

TMsgDlgType.mtInformation, mbAbortRetryIgnore, TMsgDlgBtn.mbRetry, 0,
なぜか「再試行」ボタンが表示されない!

しかも、ダイアログ右上の「閉じる」ボタンが押せなくなってる(勝手にEnabled?がFalseに設定されてしまう)。これは、キャンセルがないから、閉じるボタンはその必要がないという意味に思えてくる・・・。だから、閉じるボタンの無効化も、VCLならその方法が紹介されているんだけど、FMXでの情報は見当たらないのか・・・

実は、この記事を書こうと思ったのは、この閉じるボタンをクリックした時の戻り値が何なのか、どんなに調べても(僕が調べた範囲では)見つけることが出来なかったので、実験してみた結果を記録しておこうと思ったことがきっかけというか、はじまり。

FMX の MessageDialog を設計した人の気持ちがだんだん、わかってきた!

5.ダイアログ右上の閉じるボタンの挙動

実験結果から見えてきたこと。それは MessageDialog の閉じるボタンは、そのクリックの可否がダイアログに表示するボタンの組み合わせによって、内部的に制御されているんじゃないか?ってこと。

まず、OKボタンのみの場合、

procedure TForm1.Button9Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], TMsgDlgBtn.mbOK, 0,
      procedure(const AResult: TModalResult)
      begin
        case AResult of
          mrOK:ShowMessage('OK!:了解');
          mrCancel:ShowMessage('Cancel:取消');
          mrAbort:ShowMessage('Abort:中止');
          mrRetry:ShowMessage('Retry:再試行');
          mrIgnore:ShowMessage('Ignore:無視');
          mrYes:ShowMessage('Yes:はい');
          mrNo:ShowMessage('No:いいえ');
          mrClose:ShowMessage('Close:閉じる');
          mrHelp:ShowMessage('Help:要援助');
          mrAll:ShowMessage('All:すべて');
          mrNoToAll:ShowMessage('NoToAll:すべていいえ');
          mrYesToAll:ShowMessage('YesToAll:すべてはい');
        else
          ShowMessage(IntToStr(AResult));
        end;
      end);
  end;
end;


ダイアログ右上の「×」をクリックすると表示されたのは・・・

AResult は mrOKだった!

つまり、OKをクリックするしか、選択肢がない(未来をプログラマ自身が選択した)のだから、閉じるボタンが押された時の戻り値もmrOKでよい・・・ということか!

次に、表示するボタンを「OK」と「キャンセル」にして、デフォルト選択ボタンは「キャンセル」に指定して、再度実行。

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbCancel, 0,

右上の「×」をクリックすると・・・

AResult は mrCancel だった!

思った通り、mrCancel が設定されていた! プログラマが「キャンセルという選択肢を未来に与えた」んだから、閉じるボタンが押された時は「キャンセル」と判断してよい・・・ということ?

デフォルト選択ボタンをOKに変えてみた・・・

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbOK, 0,

右上の「×」をクリックすると・・・

AResult はmrCancel!

思った通りだ。

ボタンを「はい」・「いいえ」・「キャンセル」の3つにし、デフォルト選択を「はい」に指定して実験・・・

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbYes, 0,

右上の「×」をクリックすると・・・

AResult は mrCancel!

やっぱり、mrCancelが戻り値に設定されている!

ならば、ボタンを「はい」・「いいえ」の2つだけにすると、閉じるボタンは使用不可になるはずだ・・・。だって、プログラマの意向として、未来に「キャンセル」という選択肢は与えられていないから!

実際に動かして確認。

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo], TMsgDlgBtn.mbYes, 0,
思った通り「×」はクリックできない!

僕の中に生まれた予測は、ここで「確信」に変わった!

これはVCLのMessageDlgでも同じなんだろうか?
今度、実験だ。

6.まとめ

(1)OKボタンのみ設置したダイアログでは、閉じるボタンクリックでmrOKが返る。
(2)ダイアログにキャンセルボタンを設置した場合は、閉じるボタンもクリック可能。
(3)キャンセルボタンがある場合、閉じるボタンクリックで返る値はmrCancelになる。
(4)キャンセルボタンがない場合、閉じるボタンはクリックできない。
(5)ボタンの組み合わせは内部的に不可もある(不可でもエラーにはならない)。

7.お願いとお断り

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

範囲チェックエラーが出た時は?

{$R-}で範囲チェックさせない!

Delphiで、画像をグレースケール変換するプログラムを作成。実行すると、

表示されたエラーメッセージ

プログラムのコードは、次の通り。
Image1に表示した画像をグレースケールに変換してImage2に表示するというモノ。

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Jpeg,
  Vcl.ComCtrls;

type
  TForm1 = class(TForm)
    Image1: TImage;
    Image2: TImage;
    Button1: TButton;
    StatusBar1: TStatusBar;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

function CreateGrayScalePalette(Tone:Byte): HPALETTE;
var
  Palette: ^TLogPalette;
  i: Integer;
begin
  GetMem(Palette, SizeOf(TLogPalette) + SizeOf(TPaletteEntry) * Tone );
  Palette^.palNumEntries:=Tone+1;
  Palette^.palVersion:=$0300;
  for i := 0 to Tone - 1 do begin
    Palette^.palPalEntry[i].peRed:= Tone - i;
    Palette^.palPalEntry[i].peGreen:= Tone - i;
    Palette^.palPalEntry[i].peBlue:= Tone - i;
    Palette^.palPalEntry[i].peFlags:= 0;
  end;
  Result:=CreatePalette(Palette^);
  FreeMem(Palette);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  X, Y: Integer;
  Bmp: TBitmap;
  P: PByte;
begin
  Bmp := TBitmap.Create;
  try
   Bmp.Assign(Image1.Picture.Bitmap);
   Bmp.PixelFormat := pf8bit;
   Bmp.Palette := CreateGrayScalePalette(255);
   Image2.Picture.Bitmap := Bmp;
  finally
   Bmp.Free;
  end;
  Image2.Width:=Image2.Picture.Bitmap.Width;
  Image2.Height:=Image2.Picture.Bitmap.Height;
  Image2.Visible:=True;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  jpg: TJPEGImage;
begin
  StatusBar1.SimplePanel:=true;
  // TJPEGImageオブジェクトをインスタンス化
  jpg := TJPEGImage.Create;
  try
    // Jpegファイル読み込み
    jpg.LoadFromFile('Image.jpg');
    // Image1に割り当てる
    Image1.Picture.Bitmap.Assign(jpg);
    Image1.Width:=Image1.Picture.Bitmap.Width;
    Image1.Height:=Image1.Picture.Bitmap.Height;
    //StatusBar1.SimpleText:=IntToStr(Image1.Width)+'/'+IntToStr(Image1.Height);
  finally
    // TJPEGImageオブジェクトを破棄
    jpg.Free;
  end;
end;

end.

グレースケール変換実行のボタン(Button1)をクリックすると・・・
このButton1Click手続き内で呼び出しているCreateGrayScalePalette関数でエラーが発生。

ブレークして確認すると、エラーになるのはココ。

でも、なんでエラーになるのか、わからない・・・

Google先生に訊くと、次のような情報を発見。

[Delphi?][ネタ]透明に見えるパターンを描く

https://qiita.com/pik/items/25276e49fb131425db07

早速、範囲チェックさせないコンパイラ指令 {$R-} を追加。

ナニがどうして、そうなるのか?
原因も、理由も、皆目わからないけれど・・・

範囲チェックエラーは出なくなりました!

範囲チェックを実行しないというコンパイラ指令 {$R-} は知りませんでした。
同じ理由で困ってる方もいるかもしれないと思い、記録だけUpしました。

なお、画像のグレースケール化にあたっては、次のWebサイト様にあった情報を使わせていただきました。24bitのフルカラー画像を256階調のモノクロ画像に変換(グレースケール変換)する処理を行う際に役立つ情報が、そのアルゴリズムも含めて、多数紹介されています。

カラー画像をモノクロ画像に変換

http://rakasaka.fc2web.com/delphi/grayscale.html

また、上のWebサイト様で紹介されている配列要素を動的に確保する必要のない、Delphiが独自に定義しているTMaxLogPalette構造体を使用したCreateGrayScalePalette関数を利用した場合は、範囲チェックエラーは発生しませんでした。

お願いとお断り

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

1番が死んじゃった!

【もくじ】

1.1気筒、死んでる!
2.キャブクリーナーを吹く
3.1番復活!
4.まとめ
5.お願いとお断り

1.1気筒、死んでる!

ゼファー400のエンジン。ずっと元気だったんだけど・・・
今日、プリンタのインクを買いに行こうと思って、火を入れたら、なんか、音がヘンで、いつもならエンジンが暖まると「ドゥルルル」って軽快な連続音でアイドリングするのに、今日は「ルルル」がなくて、「ドゥル、ドゥル、ドゥル、ドゥル」って息切れしてる感じ。走り始めても、全然パワーの盛り上がりが感じられない・・・

(もしかして、1気筒死んでる?)

走れないわけじゃないけど、動けなくなってからでは手遅れになるし、ケータイも家に置いてきた。とりあえずUターンして自宅に戻り、エンジンをかけたまま、赤外線温度計でマフラーの温度を測ってみる。

4番(なんとなく測りやすいから)を最初に計測。エンジンとマフラーの接合部で200℃以上、その少し下でも150℃くらいある。とても素手では触れない。

3番も、2番も、同様。でも1番は・・・

エンジンとマフラーの接合部で75℃くらい、その少し下だと45℃くらい(写真撮ってないから説得力に欠けるけど)

ヤバイ。1番、間違いなく死んでる・・・。数km走って自宅へUターンしたから、周りから熱もらって、少しだけ暖まった・・・いや・・・調子が悪いのは、低回転の時だけで、ある程度回すと普通にフケてたような・・・そんな気もする。

・・・ってコトは、原因はもしかして、キャブのスロージェットの詰まり?

とりあえず、1番(バイクに乗った状態で見ていちばん左側の気筒)のプラグを外して、エンジンのフィンに押し当て、スタータースイッチを押してスパークすることを確認。

(大丈夫。火花は飛んでる・・・)

同時に、1番の気筒から元気よく空気が吹き出してくるのを掌に感じる。

(圧縮がないわけでもなさそう・・・)

2、3、4番は元気で、1番もスパークするから、多分電装系のトラブルじゃない。
原因はおそらく、吸気系のどこか・・・

2.キャブクリーナーを吹く

エンジンの左後ろ側をよーく見ると、ナンか液体が流れたような跡も見える・・・。コレって、もしかしてキャブのオーバーフローの跡? 気づかないうちに、また、起きてた?

とりあえず、エアクリーナーを取り外して吸気口を見てみよう。いつか、キャブがオーバーフローした時は、漏れたガソリンがたまってた・・・。

シートを外し、タンクを止めているネジ2本を外し、給油ホースを慎重に抜いて、タンクを外す。ガソリンを給油したばかりだからかなり重たい。傷つけないように、そっとカーペット(廃品をバイク用に使用)の上へ移動。で、さらにタンクを固定するためのステーを外す(このステーがあるとエアクリーナーを入れるケースのカバーが脱着できない)。

ステーを外した後、エアクリーナーケースのカバーを外して、2年前に交換したエアクリーナーと久しぶりにご対面。湿式でまだしっとりしていて、表面にほんの少しだけ埃が付いてる。ついでだから埃を払う。とりあえず、エアクリーナーはキレイになった。

LEDライトを点灯し、エアクリーナーの装着口から1番のキャブの吸気口を覗き見る。

(ガソリンは漏れてないみたい・・・)

さて、どうするか?

いちばん、イイのは、このままキャブを外して、分解・洗浄することだ。エイプ100の単気筒のキャブならやったことがあるんだけど・・・。ゼファー400の4連キャブレターは外したことはあっても、バラしたことはない。自信はまるでないけど、やるなら新品のガスケットを用意したり、それなりの準備が必要だ。素直にバイク屋さんへ、もって行こうか・・・。

(でも、せっかくここまでバラしたから、キャブレタークリーナー吹いてみようか?)

ガレージの工具箱の隣に、メンテナンス用の各種スプレー缶を並べてあるんだけど、確かその中に買い置き未使用のキャブレタークリーナーが1個あったはずだ。いつか、キャブがオーバーフローしたとき、ガソリン抜いて、給油ホースにクリーナー繋いでキャブ内にクリーナー液を噴射。しばらく放置し、さらにクリーナー液を追加(噴射)して、キャブレター内部の汚れを含んだクリーナー液を排出。これを数回繰り返したら、オーバーフローしなくなったことがあって、あの時、使わなかった残りの1缶をとっておいたんだ。

注意! 上の方法で僕のバイクのオーバーフローが直ったことは事実ですが、正しい修理方法ではありません。絶対に真似しないでください。

ガソリンの通り道が詰まってるんじゃなくて、空気の通り道が詰まってるなら、もしかしたらキャブレタークリーナー吹いたら直るかも・・・。このまま、何にもしないで元に戻すよりはいい・・・。

エンジンをかけて、バイクの左側に立ち、右手じゃなく(通常と逆の)左手でアクセルを少し開けながら、右手に持ったキャブレタークリーナーのスプレー缶のノズルを1番の吸気口に向けて、泡状のクリーニング液を噴射する。燃料じゃないから、当然、回転は落ちる。その分だけアクセルを回してエンジンを吹かす。マフラーからは大量の白煙が・・・。事情を知らない人が見たらエンジンが壊れているとしか思えないんじゃないか?

ついでに2、3、4番の吸気口にもクリーニング液を噴射。祈るような気持ちでしばらく放置して、2回、3回と繰り返す。最後に、エアクリーナーケースの底に残って溜まったクリーニング液をきれいにふき取って、清掃完了。

結果は、走ればわかる。エアクリーナーをセットしてカバーを被せ、3箇所ねじ止め。タンクのステーを付けて、タンクを装着、給油ホース等を元通りに接続。

シートを装着しようとして、ふとシートをとめる金具の可動部のあちこちにまるで油分がないことに気づく。ついでだから多目的潤滑剤のスプレー缶を持ってきて、少しずつ潤滑剤を吹きかける。それから可動部を動かして、潤滑剤をなじませる・・・。

なんだか必要以上に可動部が動く気が・・・。よく見るとシートを外すためのキー差込口自体がグラグラしてる。(なんで?)と思い、キー差込口の部品とバイクの接合部を見ると、2本あるネジがどちらも5ミリくらい緩んでる。これもついでに直す。正直、キャブの不具合の原因がどこにあるのか、それはまったくわからないけれど、こっちはネジを増し締めするだけだから簡単だ。1番のトラブルが直ったかどうかはわからないけれど、この不具合に気づいて直せただけでも、今回、キャブレタークリーナーを使った価値があったと思えてくる。あとは、エンジンが元気よく廻ってくれれば・・・。

3.1番復活!

ウェスの上に並べて置いたネジ類が全部元の場所へ戻ったことを、ウェスを眺めて確認。シートを装着して、エンジンをかけてみる。

「ドゥルルルルルルルルルルルルルルルルルルルルル」

聴きなれた、いつも通りの音だ。もしかして・・・、信じられないけど、直った?

祈るような気持ちで空ぶかし。

「フォーン!」

回転計の針は気持ちよくフケ上がる。回転の落ちもイイ。早速、乗って確認。

アクセルを開けた分だけ、思い通りに加速する。

家の近くをグルっと一周。自宅に帰って、赤外線温度計でエンジンとマフラーの接合部の温度を計測。今度は1番から4番まで、全部200℃以上になってる。

やったー! なおったー☆

この後、本来の目的だったプリンタのインクを買いに10kmくらい走ったけど、何の問題もなくエンジンは気持ちよく回る、1番は完全復活した模様。本当の原因は何だったのか、皆目わからないけれど、キャブレタークリーナーを吹いて直ったから、空気系の通り道のどこかが詰まっていたのかな? みたいな・・・

追記(20231029)

エンジンは復活したから、状況的には問題は解決。OK!なんだけれど、燃料系もキレイにしたくなって、雨の日曜日に次の作業を実行。ただし、これがメンテナンス方法として正しいか、どうか・・・と言えば、間違いなく「間違い」な気が。でも、どうしてもやってみたくなって、自己責任でやったことなので、こちらも絶対に真似しないでください。

では、ナニをしたかというと「使い残りのキャブレタークリーナーをガソリンに混ぜてキャブ内を通過させ、多少なりとも燃料系を洗浄した気になりたい」という作業。

「満タンのガソリンタンクにキャブレタークリーナーを1缶全部噴射、燃料系の汚れをゆっくり落とす」という記事がWebにあり、雨でほかにすることもなかったし、これをやってみようか・・・と思ったのです(もちろん、結果は自己責任で)。

ただ、キャブレタークリーナーは先の作業で半分くらい使ってしまい、かつ、ガソリンタンクは満タン状態なので、このままWebにあった情報を適用しても効果が薄い気がしたことと、ガソリン使いたくても外は雨で、ガソリンを減らすためだけに走る気がしなかった等々の理由から、バイクからガソリンタンクを外し、ペットボトルにタンクから抜いたガソリンを入れ、エンジンがギリギリ回るくらいの濃さでキャブレタークリーナーを相当量混ぜ、エンジンを始動、ペットボトルに入れた燃料がなくなるまでアクセルを開閉してガソリンの流量を変化させつつ、エンジンを回転させれば、クリーナーが溜まった汚れを溶かすと同時に、燃料が流れる勢いそのもので燃料系を多少なりとも洗浄できるのではないかと・・・

まず、ペットボトルのキャップ(PEだから耐ガソリン性あり)にドリルでフューエルホースと同じ径の穴を開けて、たまたまあった同じポリエチレン製のフィルターを装着したフューエルホースを通し、1.5Lのペットボトルにこのキャップを被せて、今回の清掃作業専用のガソリンタンクを製作。

フィルター下部の出っ張りがフューエルホースの内径を押し広げ、ペットボトルキャップと密着してガソリンが漏れないことを期待したが、ちょっと無理だったようで、試してみるとガソリンがわずかながらも漏れてしまう・・・。やっぱり、水とは全然違って、ガソリンの粘性ってすごく小さいんだなーって。あらためて実感。ガソリン、恐るべし。

(絶対に、引火しないように。細心の注意を払って・・・作業しなきゃ)

対応策として、キャップの外側にビニールテープを巻いてキャップとフューエルホースを一体化させ、ガソリンが漏れないようにする。

ここで心配したのは、キャップから染み出したガソリンで溶けたビニールテープの「糊」が、どれくらい燃料に混入し、キャブ内に流れ込むのか・・・ということ。実際の程はわからないけれど、いったんペットボトル外に染み出たガソリンだから、ほとんど影響はなかろーと勝手に判断。作業を進める・・・

ペットボトル側のフューエルホースと、バイクのキャブレターに繋がっているフューエルホースをどう接続するかも(やったことないから)ここで大問題化。大いに悩む。

手持ちの物品で何かないかといろいろ探してみたが、径がやや小さかったり、材質が不明で耐ガソリン性が心配だったり・・・、あれもダメ、これもダメ、そうこうしているうちに、なんか、以前、そういう目的用の「部品」を近くのホームセンター内で見たことがあるような気がしたので、家のすぐ近くのホームセンターへ直行。店内をウロウロするうちに径7mmのエア配管用の「ホース継手」を発見。(あったー! コレだ。コレだ。) 見た感じ真鍮製(?)なので耐ガソリン性に問題はない。即、購入する。

バイクからガソリンタンクを外し、キャブレター側のフューエルホースを、今、購入してきたホース継手でペットボトル側のフューエルホースに接続。ガソリンタンクから50ccくらいガソリンを抜いてペットボトルに入れ、エンジンを始動する。

ガソリンを入れたペットボトルは、針金で三脚に吊り下げて、バイクの近くにセット。

外はどしゃ降りの雨。

エンジンをかけて、しばらく暖気(キャブ車だから、暖気しないとアクセルを開けられない・・・ってか、エンジンが冷えてるうちは、アクセルを開けても回転がついてこない。でも、それが楽しくて、うれしい。バイクに乗るのは休日Onlyだから、急いで発進する必要などさらさらない。エンジンを暖める時間そのものが休日らしくて、すごく楽しい)。数分後、頼りなかったアイドリング音が少しずつ力強い音に変化する。頃合い良し。油温計は低いままだけど、きっともう大丈夫。エンジンは暖まった。試しにアクセルを開けると、結構な勢いで、ペットボトル内の燃料が減ってゆく。

(こんなにガソリンが流れるんだ・・・)

心配になるほどの減り方だ(初めて見たけど)。とりあえず、キャブレタークリーナーをペットボトル内に噴射してガソリンに混ぜ、アクセルを開け続けてクリーナーを混入したガソリンがキャブレターを通過してエンジンに入ると回転にどのような変化が生まれるのか、検証する。

ペットボトル(内のクリーナーを混入した燃料)がカラになり、透明なフューエルホース内をエンジンに向かう燃料の油面はどんどん低くなる。やがて燃料がキャブレターに吸い込まれて行くと、思った通りアイドリングが不安定になる。慌ててアクセルを開け、4000回転くらいを維持する。エンジンはなんとか止まらずに、回り続けてくれる。相当濃くクリーナーを混ぜても、回転数を上げればエンジンは止まらないようだ。

次は自分的な本番だ。ペットボトル内に300cc程度ガソリンを追加。相当濃い量になったと思われるまでキャブレタークリーナーを思い切り、大量に噴射して、ガソリンに混ぜあわせる(量は申し訳ないけど、ホントに適当)。エンジンをかけ、アクセルを開けて、クリーナーを含んだ燃料がキャブレターを通過し、エンジンが爆発不調になったことを確認していったんエンジンを停止。キャブレター内の燃料通路の汚れが多少なりとも分解されてくれることを祈りながら、このまま15分ほど休憩。

15分後、(かかってくれー)と願いながらスターターボタンを押すと、調子悪そうな感じでエンジンが始動。アクセルを開けていないとエンジンは今にも止まりそう・・・。この状態でペットボトル内にさらにキャブレタークリーナーを追加噴射。油面が下がるのを眺めつつ、エンジンが停止しないようアクセルを開け続ける。

ガレージの外は結構激しい雨。ドアを解放して、バイクをバックさせて、直管マフラーのおしまいの部分だけガレージの外へ出し、排気ガスが屋外へ出るようにして作業してるから、一酸化炭素中毒にはならないと思うんだけど、ちょっと心配。

クリーナー成分を大量に含んだガソリンがキャブレターを通過、と同時にアクセルをさらに開け続けないと回転が維持できない状態に。エンジンを吹かして4000回転程度をなんとか維持しながら、燃料が尽きてエンジンが自然に停止するのを待つ。

フューエルホース内の油面が見えなくなってからも、キャブレター内には相当量の燃料があるようだ。なかなかエンジンが止まらない。アクセルを開けながら自然停止を待つこと、数十秒。ようやくエンジンが停止する。

これで燃料系がキレイになったか、どうか、まるで確信は持てないけれど、本人が「洗浄した気になる(?)」レベルのクリーニングが完了。

フューエルホース内の燃料が見えなくなったところでエンジンを停止し、キャブレター内にクリーナー成分を含んだガソリンを滞留させて数日間置いた方がさらによかったのかもしれないけど、ガスケットその他の部品への影響が心配で、敢えてそれはしなかった・・・

雨続きで走れなかったので、実際に走れたのは作業の二日後、エンジンはスターターボタンを押すのと同時に一発でかかり、暖気後は気持ちよく回るから、とりあえず、これでよかったのかなー、みたいな。もちろん、今日も、全気筒が生きてます!

4.まとめ

キャブレタークリーナーを直接吸気口へ向けて吹く形式の簡単なクリーニングで、1気筒死んでいたエンジンが直ることあるみたいだ。本当の原因は今でもわからないけれど。

5.お願いとお断り

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

矩形検出器を改良

今までのアルゴリズムで僕の矩形検出器がユーザーに提示する「次に採点する解答欄候補」の順番は、だいたい、こんなイメージ・・・

これまでの僕のアルゴリズムでは、実際の採点順とは、まるで違う順番で
「次の採点候補」とする解答欄をユーザーへ提示してしまう・・・

今回、ほぼ採点する順番の通りに「次の採点候補」の解答欄を赤枠で囲んでユーザーに提示できるよう、解答欄矩形の座標を採点順に並べ替えるアルゴリズムを改良。その結果、矢印キー押し下げ時の「次の採点候補とする解答欄座標」を示す赤枠矩形の動きのイメージは次のようになった。

こんな僕の書いた稚拙で、頼りないプログラムでも、喜んで使ってくださる方がいる。
こんなに重たい事実はない。

プログラムが良くなることは、きっと・・・、僕自身が良くなることだ。
そう思いつつ、遅ればせながら、矩形検出器のアルゴリズムをようやく改良できた。

(矩形検出の成功率は100%で、OpenCVの性能は最高!です)

解答用紙の様式パターンの研究がまったく足りていなかったことが今回の改良が必要になったいちばんの原因。

開発の最初の段階で、解答欄矩形を余すところなく認識出来て、舞い上がってしまった自分が、幼かったんだなー。

【もくじ】

1.僕のアルゴリズムの問題点
2.解答欄をブロック化して認識処理を実行
 【間違えポイント①:範囲を指定して画像を切り出す】
 【間違えポイント②:OpenCVのfilenameはPAnsiChar型】
3.まとめ
4.お願いとお断り

1.僕のアルゴリズムの問題点

手書き答案をスキャンして得た画像データから同一設問の解答欄のみを抽出して一覧表示し、採点後、返却用答案画像に採点結果を書き戻すプログラムを書いた。

その際、スキャンした答案画像の解答欄を自動認識する矩形検出器も作成。採点プログラムに同梱して配布。同僚に使ってもらったのだけれど・・・。

解答用紙の解答欄が複数列(?)存在するような形式の解答用紙では、問題が発生。

それは、どんな問題かと言うと・・・

例えば、次のような横書き形式の解答用紙であった場合に、Myプログラムで矩形検出を実行すると・・・

検出した矩形データ(座標群)のうち、最も左上の矩形を最初に赤枠(ラバーバンドで囲って)表示、ユーザーが解答欄であるか・どうかを判定(選択)、座標自体はMemoに数値で一覧表示してあるから ↓ 矢印キーで次の矩形へ・・・という流れで、採点に必要な解答欄座標のみを取得するように設定。

解答欄と、その座標をGUIで表示する(実際の画像)。

ところが、次のような解答用紙の場合・・・

左の画像を右へコピペしたので、設問番号がオカシイのは無視してください・・・

・・・のですが、矩形検出を実行後、検出された座標群から、僕のアルゴリズムで解答欄の選択を実行すると・・・

解答欄矩形の座標自体は、確実に取得できているから、矢印キーを駆使して、採点したい向きに解答欄の座標が並ぶように、座標を選択していけばいいだけの話なんだけれど。

これが・・・

超絶。すーぱーめんどくさい!

さらに、どんなにまっすぐ解答用紙をセットしても、必ず右肩上がり(画像が左に0.05度くらい傾いた状態で)でスキャンしてくれるという、メインで使用しているスキャナーならではのヘンなクセもあり、しかも、その画像に対して「Y座標の小さい順に赤枠で囲む」という僕のアルゴリズムは「正しく」機能するから、解答欄を上から下へ、行単位では左から右へという夢見た処理の流れは完全に逆転。採点候補の解答欄は左右に飛び、行単位でも右から左へ、想定とは真逆の順番で次の採点候補矩形が延々と表示される結果に・・・。こんな状態で、提示(表示)された解答欄矩形の座標を、採点順に正しく選択することは、年配の同僚にはほぼ不可能・・・

解答欄矩形、それ自体は 100% 正しく検出できているのですが・・・
あまりにも、こちらの気持ちを無視したプログラムの挙動を目の当たりにして・・・

責任者を出せ!って
怒鳴りたくなるんだけど・・・

ちょっと・・・、待って。

責任者。オレじゃん、
みたいな・・・

解答用紙によっては、さらに・・・

この例だと、Y座標がムチャだから、解答欄の選択作業はさらに困難を極め・・・

もっと・・・、発展(?)して

もぉ T_T

2.解答欄をブロック化して認識処理を実行

どうすればいいか?

答えはひとつしかありません。そうです!

解答用紙の解答欄を、採点順になるよう「まとまりのブロック」に分けて、ブロックごとに解答欄座標の取得手続きを行えばいいのです。

これで「一部がCutされた解答欄」は、矩形として認識されないので、最後に手動で座標データを追加すれば、なんとかなります(手動設定のプログラムは期待通りに動作しているから安心だし、上の図のような特殊な解答欄は最後の方にある場合が多い)。

この新方針のもとで、プログラムを見直してみると・・・

もともとのアルゴリズムは画像全体を1ブロックとして扱って、解答欄の矩形座標を検出しているから・・・

procedure TForm1.btnGetSquareClick(Sender: TObject);
var
  //PythonのScriptを入れる
  strScrList:TStringList;
  //Pythonから送られたデータを保存する -> グローバル変数化
  //strAnsList:TStringList;
  //Sort
  i,j:integer;
  //strFileName:string;
  strList:TStringList;
begin

  //画像分割処理なし(初期状態)

  //初期化
  Memo1.Clear;
  //Scriptを入れるStringList
  strScrList:=TStringList.Create;
  //結果を保存するStringList
  strAnsList:=TStringList.Create;

  try
    //Python Script
    strScrList.Add('import cv2');
    strScrList.Add('import numpy as np');
    strScrList.Add('from PIL import Image');
    //strScrList.Add('img = cv2.imread("./ProcData/sample2.jpg")');
    //strScrList.Add('img = cv2.imread(r"'+StatusBar1.SimpleText+'")');
    strScrList.Add('pil_img = Image.open(r"'+StatusBar1.SimpleText+'")');
    strScrList.Add('img = np.array(pil_img)');
    strScrList.Add('gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)');
    strScrList.Add('gray = 255 - gray');
    strScrList.Add('ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)');
    strScrList.Add('contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)');
    strScrList.Add('contours = list(filter(lambda x: cv2.contourArea(x) > '+cmbThreshold.Text+', contours))');
    strScrList.Add('for i in range(len(contours)):');
    strScrList.Add('    im_con = img.copy()');
    strScrList.Add('    x, y, w, h = cv2.boundingRect(contours[i])');
    strScrList.Add('    var1.Value =str(x)+","+str(y)+","+str(x+w)+","+str(y+h)');
    //Scriptを表示
    Memo1.Lines.Assign(strScrList);
    //「0による浮動小数点数除算」のエラーを出ないようにするおまじない
    MaskFPUExceptions(True);
    //Execute
    PythonEngine1.ExecStrings(Memo1.Lines);
    //結果を表示
    Memo2.Lines.Assign(strAnsList);
  finally
    //StringListの解放
    strAnsList.Free;
    strScrList.Free;
  end;

end;

・・・という感じで、かなりシンプル!

このあと、横書き・縦書きという解答欄の書き方に応じて、解答欄座標の並べ替えを行っている。どちらかというと、解答欄矩形の検出作業はOpenCVにおまかせで、並べ替えのアルゴリズムの方を工夫した記憶が・・・。

とりあえず・・・

横書き解答用紙が選択された時のみ、解答欄を何ブロックに分割して処理するか、GUIで選択できるようにして・・・

ブロック数分Loopを廻す中で、OpenCVのcvSetImageROI関数を用いて答案画像を分割、結果を一時Memoに書き込んで、2ブロック以降の座標値に対しては、そのx座標を答案画像上での値に修正(←実はコレを忘れていて、動作確認の際 ??? なことになり、初めて取得した座標値の修正作業の必要性に気づく)、で、最後に採点する順番になるよう座標を並べ替えてユーザーに提示する準備を実行。さらに、横書き解答用紙の場合は、一時Memoから座標データ提示用Memoへデータを移動して終了。・・・みたいな手続きのカタチにプログラムを半日程度かけて修正。

【間違えポイント①:範囲を指定して画像を切り出す】

cvSetImageROI関数の使い方は、その内部に入れてるcvRect関数の第1引数が切り出し位置の左上x座標、第2引数が切り出し位置左上のy座標、第3引数が切り出す幅、第4引数が切り出す高さとなっている。

最初、よく考えずにcvRect関数の第3、4引数を切り出し位置右下のx、y座標だと思い込んで設定し、切り出した画像の幅が変化することから、設定の誤りに気づく。前にマークシートリーダーを開発した時(Python環境を導入する前の段階で)、Windows用のOpenCVで画像処理していたときに、この関数のお世話になったはずなんだけど、もぉすっかり忘れてしまっていたようです。

  //指定範囲の画像を切り出して保存
  //cvRect(x, y, Width, Height)
  cvSetImageROI(sourceImage, cvRect(top_x, top_y, xWidth, yHeight));

【間違えポイント②:OpenCVのfilenameはPAnsiChar型】

それから、画像データへのPathとファイル名を入れる変数p1が PAnsiChar 型であることを、こちらもすっかり忘れていて、String型で引数を指定してエラーになって初めてそれを思い出す。変数に値を代入する際、いったん AnsiString 型でキャストして更に PAnsiChar でキャスト。

  //画像データのファイル名
  p1:PAnsiChar;

begin

  ・・・

  //String 型の文字列を PAnsiChar 型の文字列に変換
  //AnsiString 型でキャストして更に PAnsiChar でキャスト
  p1:=PAnsiChar(AnsiString('CutImage0'+IntToStr(i)+'.jpg'));

  //画像を保存する
  cvSaveImage(p1, sourceImage);

  ・・・

end;

完成した手続きがこちら(変数名等は思いつくまま、意図した通りに動けば 可 とした)

procedure TForm1.btnGetSquareClick(Sender: TObject);
var
  //PythonのScriptを入れる
  strScrList:TStringList;
  //Pythonから送られたデータを保存する -> グローバル変数化
  //strAnsList:TStringList;
  //Sort
  i,j:integer;
  //strFileName:string;
  strList:TStringList;

  //画像の等幅分割
  //切り出し領域
  top_x, top_y:integer;
  yHeight:integer;
  //xの増分
  xWidth, iMax:integer;
  //for Imageの読み込み
  sourceImage: PIplImage;
  //画像データのファイル名
  p1:PAnsiChar;

  //x座標の補正
  str1, str2, str3, str4:string;

begin

  //画像分割処理なし(初期状態)
  {
  //初期化
  Memo1.Clear;
  //Scriptを入れるStringList
  strScrList:=TStringList.Create;
  //結果を保存するStringList
  strAnsList:=TStringList.Create;

  try
    //Python Script
    strScrList.Add('import cv2');
    strScrList.Add('import numpy as np');
    strScrList.Add('from PIL import Image');
    //strScrList.Add('img = cv2.imread("./ProcData/sample2.jpg")');
    //strScrList.Add('img = cv2.imread(r"'+StatusBar1.SimpleText+'")');
    strScrList.Add('pil_img = Image.open(r"'+StatusBar1.SimpleText+'")');
    strScrList.Add('img = np.array(pil_img)');
    strScrList.Add('gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)');
    strScrList.Add('gray = 255 - gray');
    strScrList.Add('ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)');
    strScrList.Add('contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)');
    strScrList.Add('contours = list(filter(lambda x: cv2.contourArea(x) > '+cmbThreshold.Text+', contours))');
    strScrList.Add('for i in range(len(contours)):');
    strScrList.Add('    im_con = img.copy()');
    strScrList.Add('    x, y, w, h = cv2.boundingRect(contours[i])');
    strScrList.Add('    var1.Value =str(x)+","+str(y)+","+str(x+w)+","+str(y+h)');
    //Scriptを表示
    Memo1.Lines.Assign(strScrList);
    //「0による浮動小数点数除算」のエラーを出ないようにするおまじない
    MaskFPUExceptions(True);
    //Execute
    PythonEngine1.ExecStrings(Memo1.Lines);
    //結果を表示
    Memo2.Lines.Assign(strAnsList);
  finally
    //StringListの解放
    strAnsList.Free;
    strScrList.Free;
  end;
  }

  //画像分割処理ここから

  //初期化
  //Memo1.Clear;
  Memo2.Clear;
  MemoTemp.Clear;

  //初期化(定数的に利用する)
  top_y:=0;

  //分割数
  iMax:=StrToInt(cmbPartition.Text);

  //結果を保存するStringList
  strAnsList:=TStringList.Create;

  //初期化
  xWidth:=0;

  try

    for i := 0 to iMax-1 do
    begin

      //画像を読み込む(Bitmap・JPEGどちらも読み込み可能)
      p1:=PAnsiChar(AnsiString(StatusBar1.SimpleText));
      sourceImage := cvLoadImage(p1, CV_LOAD_IMAGE_ANYDEPTH or CV_LOAD_IMAGE_ANYCOLOR);

      //intとTruncは小数点以下を切り捨て。異なるのは、戻り値がintは実数、Truncは整数になること
      xWidth:=Trunc(SimpleRoundTo(sourceImage.Width/iMax,0));
      yHeight:=sourceImage.Height;

      //切り出す座標を指定
      top_x:= xWidth * i;

      try

        //指定範囲の画像を切り出して保存
        //cvRect(x, y, Width, Height)
        cvSetImageROI(sourceImage,cvRect(top_x, top_y, xWidth, yHeight));

        //String 型の文字列を PAnsiChar 型の文字列に変換
        //AnsiString 型でキャストして更に PAnsiChar でキャスト
        p1:=PAnsiChar(AnsiString('CutImage0'+IntToStr(i)+'.jpg'));
        //画像を保存する
        cvSaveImage(p1, sourceImage);

      finally
        //イメージの解放
        cvReleaseImage(sourceImage);
      end;

    end;

    for i := 0 to iMax-1 do
    begin

      //Scriptを入れるStringList
      strScrList:=TStringList.Create;

      //x座標の補正値を計算
      top_x:= xWidth * i;

      try
        //Python Script
        strScrList.Add('import cv2');
        strScrList.Add('import numpy as np');
        strScrList.Add('from PIL import Image');
        //strScrList.Add('img = cv2.imread("./ProcData/sample2.jpg")');
        //strScrList.Add('img = cv2.imread(r"'+StatusBar1.SimpleText+'")');
        //strScrList.Add('pil_img = Image.open(r"'+StatusBar1.SimpleText+'")');
        strScrList.Add('pil_img = Image.open(r"'+'CutImage0'+IntToStr(i)+'.jpg'+'")');
        strScrList.Add('img = np.array(pil_img)');
        strScrList.Add('gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)');
        strScrList.Add('gray = 255 - gray');
        strScrList.Add('ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)');
        strScrList.Add('contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)');
        strScrList.Add('contours = list(filter(lambda x: cv2.contourArea(x) > '+cmbThreshold.Text+', contours))');
        strScrList.Add('for i in range(len(contours)):');
        strScrList.Add('    im_con = img.copy()');
        strScrList.Add('    x, y, w, h = cv2.boundingRect(contours[i])');
        strScrList.Add('    var1.Value =str(x)+","+str(y)+","+str(x+w)+","+str(y+h)');
        //Scriptを表示
        Memo1.Clear;
        Memo1.Lines.Assign(strScrList);
        //「0による浮動小数点数除算」のエラーを出ないようにするおまじない
        MaskFPUExceptions(True);
        //Execute
        PythonEngine1.ExecStrings(Memo1.Lines);
        //結果を表示
        if RadioButton1.Checked then
        begin
          //x座標を補正する
          MemoTemp.Lines.Assign(strAnsList);
          if i<>0 then
          begin
            for j := 0 to MemoTemp.Lines.Count-1 do
            begin
              //値を取得
              str1:=GetTokenIndex(MemoTemp.Lines[j],',',0);
              str2:=GetTokenIndex(MemoTemp.Lines[j],',',1);
              str3:=GetTokenIndex(MemoTemp.Lines[j],',',2);
              str4:=GetTokenIndex(MemoTemp.Lines[j],',',3);
              //カンマ区切りの文字列の1,3番目にtop_x値を加える(座標を修正)
              str1:=IntToStr(StrToInt(str1)+top_x);
              str3:=IntToStr(StrToInt(str3)+top_x);
              //書き戻し
              MemoTemp.Lines[j]:=str1+','+str2+','+str3+','+str4;
            end;
          end;
        end else begin
          Memo2.Lines.Assign(strAnsList);
        end;
      finally
        //StringListの解放
        //strAnsList.Free;
        strAnsList.Clear;
        strScrList.Free;
      end;

      //横書きの場合のみ実行
      if RadioButton1.Checked then
      begin

        //strFileName:=ExtractFilePath(StatusBar1.SimpleText)+'Temp.csv';
        //MemoTemp.Lines.SaveToFile(strFileName);

        strList := TStringList.Create;
        try
          for j := 0 to MemoTemp.Lines.Count-1 do
          begin
            strList.Add(MemoTemp.Lines[j]);
          end;
          //並び替え 降順 -> True
          //if RadioButton1.Checked then
          //begin
            fAscending := False;
            fIndex := 1; //2番目の項目を
            fStyle := ssInteger; //整数型でソート
            strList.CustomSort(MyCustomSort); //ソート開始
          //end else begin
          //  fAscending := True;
          //  fIndex := 0; //1番目の項目を
          //  fStyle := ssInteger; //整数型でソート
          //  strList.CustomSort(MyCustomSort); //ソート開始
          //end;

          //データ抽出
          //Memo2.Clear;
          for j := 0 to strList.Count - 1 do
          begin
            Memo2.Lines.Add(strList[j]);
          end;
        finally
          MemoTemp.Clear;
          strList.Free;
        end;
      end;
    end;

  finally
    //StringListの解放
    strAnsList.Free;
  end;

  //画像分割処理ここまで

  //縦書きの場合のみ実行
  if RadioButton2.Checked then
  begin

    //strFileName:=ExtractFilePath(StatusBar1.SimpleText)+'Temp.csv';
    //Memo2.Lines.SaveToFile(strFileName);

    strList := TStringList.Create;
    try
      for i := 0 to Memo2.Lines.Count-1 do
      begin
        strList.Add(Memo2.Lines[i]);
      end;
      //並び替え 降順 -> True
      //if RadioButton2.Checked then
      //begin
      //  fAscending := False;
      //  fIndex := 1; //2番目の項目を
      //  fStyle := ssInteger; //整数型でソート
      //  strList.CustomSort(MyCustomSort); //ソート開始
      //end else begin
        fAscending := True;
        fIndex := 0; //1番目の項目を
        fStyle := ssInteger; //整数型でソート
        strList.CustomSort(MyCustomSort); //ソート開始
      //end;

      //データ抽出
      Memo2.Clear;
      for i := 0 to strList.Count - 1 do
      begin
        //Memo2.Lines.Add(GetCommaText(strList.Strings[i],fIndex));
        Memo2.Lines.Add(strList[i]);
      end;
    finally
      strList.Free;
    end;

  end;

  if RadioButton2.Checked then
  begin
    ScrollBox1.HorzScrollBar.Position:=ScrollBox1.HorzScrollBar.Range;
  end else begin
    //ScrollBarが表示されていなくてもエラーにならない
    ScrollBox1.HorzScrollBar.Position:=0;
  end;

  //表示
  LBRow.Visible:=True;
  LBRow2.Visible:=True;

  //操作可能に設定
  btnOpen.Enabled:=True;
  btnSave.Enabled:=True;

  //操作不可に設定
  btnGetSquare.Enabled:=False;

  //先頭へスクロール
  Memo2.Perform(WM_VSCROLL,SB_TOP,0);

  //先頭行へ
  Memo2.SelStart:=SendMessage(Memo2.Handle, EM_LineIndex, 0, 0);
  Memo2.Perform(EM_SCROLLCARET, 0, 0);  //キャレット位置までスクロール
  Memo2.SetFocus;

  GetLinePos;

  //矩形を表示
  Memo2Click(Sender);

end;

ちなみに、最後の解答欄矩形を表示する処理は・・・

procedure TForm1.Memo2Click(Sender: TObject);
var
  i:integer;
  //x1,x2,x3,x4:integer;
  //y1,y2,y3,y4:integer;
  p1,p2:TPoint;
  //文字列切り分け///////////////////////////////////////////////////////////////
  function RemoveToken(var s:string;delimiter:string):string;
  var
    p:Integer;
  begin
    p:=Pos(delimiter,s);
    if p=0 then Result:=s
    else Result:=Copy(s,1,p-1);
    s:=Copy(s,Length(Result)+Length(delimiter)+1,Length(s));
  end;

  function GetTokenIndex(s:string;delimiter:string;index:Integer):string;
  var
    i:Integer;
  begin
    Result:='';
    for i:=0 to index do
      Result:=RemoveToken(s,delimiter);
  end;

begin

  if not EditTF then
  begin

    //座標を取得
    i:=Memo2.Perform(EM_LINEFROMCHAR, Memo2.SelStart, 0);
    //ShowMessage(IntToStr(i));

    //エラー対策
    if Memo2.Lines[i]='' then Exit;

    x1:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',0));
    y1:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',1));
    x2:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',2));
    y2:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',3));

    if Assigned(plImage1) then begin
      FreeAndNil(plImage1);
    end;

    //コンポーネントを生成し,イベントを定義し,位置を指定して画像を表示
    plImage1:=TplResizeImage.Create(Self);
    plImage1.Parent:=ScrollBox1;
    plImage1.TransEvent:=True;
    //クライアント座標をスクリーン座標へ変換
    //GetSystemMetrics(SM_CYCAPTION) -> タイトルバーの高さ
    //GetSystemMetrics(SM_CYFRAME) -> ウィンドウの枠幅
    p1.X:=x1-(GetSystemMetrics(SM_CYFRAME) div 2);
    p1.Y:=y1-GetSystemMetrics(SM_CYCAPTION)-(GetSystemMetrics(SM_CYFRAME) div 2);
    p2.X:=x2-(GetSystemMetrics(SM_CYFRAME) div 2);
    p2.Y:=y2-GetSystemMetrics(SM_CYCAPTION)-(GetSystemMetrics(SM_CYFRAME) div 2);
    p1:=Image1.ClientToScreen(p1);
    p2:=Image1.ClientToScreen(p2);
    plImage1.SetBounds(p1.X, p1.Y, p2.X-p1.X, p2.Y-p1.Y);

    //SelectedプロパティをTrueにするとラバーバンドとグラブハンドルが表示される
    plImage1.Selected := True;
    plImage1.BringToFront;

  end;

end;

最終的に完成したコードはまわりくどくて、汚いけど、動きは期待したとおり、例えば3ブロックある解答用紙での処理は・・・

1ブロックめの最初。

このまま、下方向へ解答欄矩形の座標データを選択して、いちばん下の座標まで移動すると、次の矢印キー押し下げと同時に赤枠は2ブロックめの先頭へ移動。

2ブロックめは上の例だと2列分あるので、ちょっと処理が面倒だけど、実際の解答用紙ではこんな例はまずないので大丈夫ということにしておいて、とりあえず、いちばん下の座標まで移動したところで次の矢印キー押し下げ、同時に赤枠は3ブロックめの先頭へ移動。

で、3ブロックめの解答欄矩形も余すところなく、選択。実に、イイかんじ。

コレだ! コレだ!!
コレを実現したかったんだ☆

やったー!!
できた!!!

3.まとめ

複数ブロックからなる解答用紙の解答欄矩形検出は(考えてみれば当たり前ですが)、次のように処理するとうまく行きます。

(1)解答用紙の画像を予め複数ブロックに分割して別画像として保存
(2)それぞれのブロックごとに解答欄の矩形を検出&採点する順番に並べ替え
(3)ブロックごとに取得した座標値を解答用紙画像全体の中での座標値に変換
(4)全座標値を結合して定義ファイル等に保存

今回は、上の(3)の処理を失念してプログラミングしていたので、必要だった修正は、ブロックごとの値として取得したx座標を、解答用紙画像全体の中でのx座標に変換する処理を追加するだけという、この修正わずか1回で期待したとおりに動作するプログラムを完成できました。これは僕的には極めて稀有な例で、言うのも恥ずかしい事実ですが、いつも七転八倒状態を延々と繰り返してなんとか思ったとおりの動作を実現しているので、たまにはこんなコトがあってもいいかなー。みたいな♪

4.お願いとお断り

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

風を切ろう

ゼファー400のレストア動画を見た。

13編全部、最高だったけど。

何より胸に残ったのは・・・

「もう一度、風を切ろう・・・」って、言葉。

この言葉のほんとうは、二輪車乗りにしか、わからないんじゃないかな・・・

( いい言葉だな )って、

つい ・・・ 瞳がうるんでしまった。

ドキドキしながら

きらめく風の中・・・

初めてアクセルを開けた日・・・

僕は、まだ16歳だった・・・。

このレストア動画の中に・・・ そんなシーンは、1秒もないのに。

なぜか、懐かしい・・・風の匂いまで、僕は思い出せた・・・。

「必死だった」って、

レストアシリーズとは別の動画で、この動画の作者は、実にさりげなく語ってたけど、

こんなに「オートバイが好き」って、その気持ちが伝わってくる映像を・・・

僕は、これまでに、ほんとうに・・・ ほんとうに見たことがない。

もちろん、これからも、ないだろう。

そう、断言できるくらい、心が動いたから、言う。

しせい、てんにつうず。

相手が人でなく、それが「機械」であっても・・・ それはきっと同じ。

真心を伝えたい 相手が、人でなく、たとえ機械であったとしても、

伝えたい「想い」が、間違いなく、「ほんとう」なら・・・

嘘も、偽りもない、その「想い」に勝るものなんか、絶対にない。

だから、敬意を込めてその総集編へのリンクを、ここに貼ります。

ふみっちょさん。心から、本気で、ほんとうの、ありがとうです。

あなたの動画を見て、

オートバイに乗りたかった気持ちの原点を、僕は、思い出しました。

16歳だった・・・ 初めて、風を切った日の・・・

アクセルを開けた、あの一瞬の・・・記憶。

そう。あの瞬間から、僕は・・・ オートバイが大好きに・・・

そんな想いを抱きしめて、眠ったから・・・ かな・・・?

8月のある朝、目が覚めていちばん最初に思い出したのは・・・

まだ、バイクで行ったことのない、家のすぐ近くにある、大好きな場所だった。

何年も前から、クルマでは、何度も訪れた場所なのに、なぜか・・・

そこへは一度も、バイクで行ったことはなかった・・・。

( 今日のために、とっておいた? )

まぁ、いい。今、考えるのはよそう。

人生には、いろんな不思議があっていい。

きみと、そこへ行こう。

そう、風を切って。

何度も、何度も、ここで、近くの空港に降りる飛行機を数えた。
初めてここへきたきみは、はしゃいでるみたいに輝いて。

でも、33歳。

いっぱい、壊れたよな・・・

フロントフォークからのオイル漏れ
キャブレターのオーバーフロー

そうだ、コンビニへ入ろうとしたら
サイドスタンドが落ちてなかったこともあった。

ウインカーのプラスチックが経年劣化して、根本から折れたことも
リアショックのオイルが全部抜けたこともあった。

セルが廻らなくなったことも
ブレーキが噛んだことも

立ちゴケしてクラッチレバーを折ったり、
エンジンを傷つけたこともあった・・・

レバーは交換すればよかったけど
エンジンは、涙目になりながら、必死で磨いて再生したんだ・・・

そうだ

カムチェーンテンショナーの押しが足りなくなって
アクセル戻す度にエンジンからすごいガラガラ音が聞こえてきた時は
心が折れそうになったな・・・

やっとの思いで、一度も外したことがないキャブレターを外して・・・
取り出せたカムチェーンテンショナーは、Webで見た写真とは全然違う旧型で・・・
どうやって調整したらいいのか、
まったく、わからなかった。

自分でもなんとかなりそうな、C3型以降用(?)のテンショナーを手に入れて・・・
ノッチの押し込みを調整しながら・・・震える手で装着。
汗まみれになってキャブレターを元に戻し、
祈るような気持ちで、スターターボタンを押したんだ。

エンジンのガラガラ音が完全に消えた時は、天にも昇る心地だった。

でも、熱にさらされる場所は、どうしても酸化が進んで・・・

何度も磨いて、耐熱塗料を塗り直したマフラー。
それほど高温にならない後ろ部分は耐熱塗料でなくてもOKなことを
経験から知ったけど。

さすがに、エンジンに、それは通用せず・・・
オリジナルの塗装は、もうほとんど残ってない・・・。

みんな、サビとの戦いで、消えてしまった・・・

でも、まだ、走れるよね。

走れる限り、ふたりで、風を切ろう。

もうすぐ、秋だね。

ふたり、黄金色の風の中、

アクセルを開けて・・・

どこまでも、

そして、いつまでも・・・

風を切って・・・

走ろう☆

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.お願いとお断り

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