2020/07/27

Book内で完結するフォーム表示のHelp画面




1.背景

Excelでシステムを作った後工程として、その使い方やFAQ(よくある質問)を取扱説明書にしたり教育したりするのは、プログラムを考える以上に大変な仕事です。システム立ち上げ以降も電話やら呼出しやらで時間を多く取られる事も良くあります。
それらをゼロに出来るとは思えませんが、少しでも減らすために「Help画面を作る」のは対策の1つです。少なくとも時間稼ぎをするくらいの効果はあるはずです。
ということで、今回はHelp画面をフォームで表示させる手法について説明します。

2.概要

Excelを使ったシステムがあり、画面(シート)ごとに機能が分かれていると仮定します。
不明な点があった場合にはユーザーが「WindowsのヘルプキーであるF1キー」を押すと、図2-1のような「Help選択ダイアログ」が表示され、「アプリのHelp画面」or「WindowsのHelp画面」を選択します。
F1キーを押した時のHelp選択ダイアログ
図2-1

Help選択ダイアログで「アプリのHelp画面」を選択すると、図2-2のようなHelp内容がダイアログ上に現れます。Help内容は、使っている画面内容に合った内容がまず表示され、ボタンによって異なるページを開くことも可能です。
システムHelp画面その1 システムHelp画面その2
図2-2

また、Helpを表示しているダイアログは、通常のブラウザと同様にマウスでサイズを変更出来ます。
(図2-3は実際にはあり得ない画面ですが、ダイアログをマウスでサイズを変更している事を表したものです。)
Helpダイアログはマウスでサイズ変更可
図2-3

なお、Declare宣言を64ビットExcelに対応させる修正を加えて試行してみた結果、「Windows11 + Excel2021 64Bit版」では、「ユーザーフォームが起動する時にExcelが落ちてしまう」現象が発生することが分かりました。
理由が「Windows11」なのか「Excel64」なのか、または「私のPC特有」なのかは分からないのですが、とりあえず御注意願います。
(2023.01.29)

3.プログラムの概要

3-1.Help表示の手法について

今回は、事前にHTML文を作成しておき、それをフォーム上に表示することでHelp画面としています。
まず、「ExcelのシステムからHelp画面を表示」する手段として、どのようなものがあるかを整理します。

ExcelのVBAを使って起動できる外部アプリは多々あり、Helpとして使えそうなものだけでも、IE(Internet Explorer)、MS-Word、PDF(Acrobat Reader他)などが考えられます。
しかし、そのHelpファイルを開けるアプリがPCにインストールされていなくてはならず、今回はExcelのみで完結できるという考え方でフォームを使う事としました。環境によっては外部アプリを使用した方が簡単な場合もありますので、「Excelで完結する」ことを前提にしない方が良いと思います。

次に、フォームでHelp表示するとして、どのコントロールを使うかを考えます。文章を表示できそうなコントロールを図3-1で比較してみます。
コントロール使用法メリット・デメリット
Label設計時に直接記入長い文章を全て見せるには、フォームサイズが大きくなる
マクロで記入スクロールバー等との組合せで頁送りが可
TextBoxマクロで記入TextBoxのスクロールバー表示可
WebBrowserHTMLデータを指定ジャンプ可。文字サイズ大小可。彩色可。画像挿入可
図3-1

図3-1の通り「外部アプリと同等のHelp」を狙うのであれば、HTML機能を使えるWebBrowserが良いと考えました。
ではHTMLデータをBook内に保管する方法ですが、以下のようなものがあると思います。
保管場所サイズ制限メリット・デメリット
セル内にHTMLを記述~32,767文字縦長のHTMLの全容を掴み難い
改行はCtrl+Enter要
String型定数でマクロ中に記述~2GB(約10億文字?)マクロ内では"(ダブルクォーテーション)
の扱いが異なる
ワークシート上のテキストボックス内に記述不明(30万文字以上)テキストボックス内は、Enterキーが効く
但しタイプミス発見し難い
ワークシート上にテキスト埋込オブジェクトとして貼付60KB(メモ帳の制限)トライしたが、直接テキストを
取り出せそうに無い
図3-2

別のエディタでHTMLデータを作り、それを貼り付けるのであれば、①でも③でも同じと思います。但し②は、そのまま貼り付けると「"(ダブルクォーテーション)」の意味が変わってしまいますので、修正が発生します。
今回は、HTMLデータを貼り付けた後でも修正が楽な③を採用しました。もちろん状況によっては①③にするのも有りと思います。

3-2.今回システムの概要

事前にHelpボタンである「F1キー」にマクロを登録しておき、ユーザーが押すことで分岐のダイアログが出るようにしています。また入力画面ではないワークシート(Sheet1)上にテキストボックスを置き、複数のHTML文書を保存しておきます。

ユーザーが「F1キー」を押し「アプリ側のHelp」に分岐すると、現在開いているシート名を調べ、適切なHTML文書をフォーム上のWebBrowserコントロールに書き込み表示させます。(図3-1)
Helpダイアログを表示する流れ
図3-3

一方「標準Help」を選択した際には、Excelの標準Helpを表示します。
またExcelのフォームのサイズは、標準ではユーザー側からは変更出来ませんので、Windows APIを使って変更可能にしています。

4.ワークシート上のHTMLデータ

システム入力に使用していないワークシート(ここではSheet1)上に図形のテキストボックスを置き、そこに「Help内容」を「HTML言語」で記入します(図4-1)。または、他のエディタで作ったHTMLデータをテキストボックスに貼り付ける方法でもOKです。
Help内容をテキストボックスに記入
図4-1

テキストボックス内に記載するHelp内容は「<body>から</body>の間のみ」です。
HTML言語の文法については割愛しますが、ページ内リンクを含めた他ページへのリンク( a タグ)、画像の貼付け(img タグ)、Styleタグによる装飾も可能です。

尚、PngやJpg等の画像ファイルをアドレス指定できない場合は、「画像をBase64でテキストデータにエンコード(変換)し、src属性にアドレスの代わりに指定」する方法もあります。(図4-1の左テキストボックスの中央付近)
画像のBase64変換は、様々なサイトで出来ますので捜してみて下さい。

作られたばかりのテキストボックスには「TextBox 1」のような名前ついています。(右上の名前ボックス欄に表示される名前は「テキスト ボックス1」の様なカタカナになります。)
この自動的に付けられた名前を、自分が分かり易い名前に変更しておくと管理が楽になります。

今回は図4-2の様に、目的のテキストボックスをアクティブにした状態で左上の「名前ボックス」をクリックすることで、管理し易い名前に変更します。すると、テキストボックスのNameプロパティが変更され、呼び出す時も「Sheet1.Shapes(新たに付けた名前)」で呼び出せます。
テキストボックスの名前を変更
図4-2

システム入力画面が仮に2つ(Sheeet2とSheet3)あり、それぞれにHelp画面を作るとすると、2つのテキストボックスを設けてHTMLデータを記入します。(今回は、1つ目のシステムHelpの名前をhtml-01、2つ目をhtml-02としました。)

5.フォームへのコントロールの配置

フォーム(UserForm1)上には、CommandButton2つとWebBrowser1つを図5-1のように配置します。
フォームへのコントロールの配置
図5-1

WebBrowserコントロールは「初期のツールボックスには表示されていません」ので、ツールボックス上で右クリックし「コントロールの追加リスト」の中から「Microsoft Web Browser」にチェックを入れて「フォームのコントロールのWebBrowserのマーク」印を追加します。

2つのCommandButtonの表面の文字(Caption)はマクロ側で変更しますので、そのままでもOKです。
また、CommandButtonのすぐ下にWebBrowserを配置します。WebBrowserの幅・高さは自動的に調整しています。

6.フォームコード

6-1.フォーム起動の準備

フォームの起動中に実行されるのが図6-1になります。最初にInitializeイベント、次にActivateイベントが発生します。
  1. '========== ⇩① フォームの初期化(Initializeイベント) ===================
  2. Private Sub UserForm_Initialize()
  3.  Me.WebBrowser1.Navigate ""
  4.  Me.WebBrowser1.Left = 0
  5.  Me.CommandButton1.Caption = "Help①"
  6.  Me.CommandButton2.Caption = "Help②"
  7. End Sub
  8. '========== ⇩② フォームの初期化(Activateイベント) ========
  9. Private Sub UserForm_Activate()
  10.  Call FormResize              '// フォームのリサイズを可能にする
  11.  Call UserForm_Resize           '// WebBrowserのサイズを調整
  12.  Do While WebBrowser1.Busy = True Or WebBrowser1.ReadyState <> 4   '//WebBrowserの準備待ち
  13.   DoEvents
  14.  Loop
  15.  If ActiveSheet.Name = "Sheet2" Then   '// 開いているシートでHelpを分ける
  16.   Call WebStart("html-01")
  17.  Else
  18.   Call WebStart("html-02")
  19.  End If
  20. End Sub
図6-1

Initializeイベントでは、フォームの普遍的な設定をします。
3行目ではWebBrowserのNavigateメソッドで「""(長さゼロの文字列)」を渡しています。本来の使い方であれば、このNavigateメソッドに表示するページを指定するのですが、今回は表示内容を直接プロパティに書き込みますので、このようにWebBrowserコントロールに空文字列を読み込ませます。

尚、今回の方法では、フォーム起動時に一瞬「このページを表示できません」とエラー画面が出ることになります。これを嫌って「白画面」にしようと「WebBrowser1.Navigate "about:blank"」を使うと、「ページ内リンクが不能」になってしまいますので注意下さい。
原因はページの情報として「blank」という文字列が入ってしまうためのようです。

4行目では、WebBrowserコントロールの左右方向の位置をフォームの左端に密着させます。このWebBrowserの位置は、最終的には図6-8の「UserForm_Resizeイベント」で「ボタンを除いたフォーム全体をWebBrowserが占める」形に整えられます。
尚、WebBrowserコントロールのサイズを決める機能が2つ(Initialize と UserForm_Resize)に分かれたのは、今回フォームのサイズをユーザーが変更できるようにした為です。
Initialize側では「WebBrowserの位置(左上のポイント位置)」を決め、UserForm_Resize側では「WebBrowserの幅と高さ」を決めています。

6~7行目は、ボタンの表面文字の変更をしています。

11~24行目はActivateイベントの処理です。
まず12行目は、標準モジュールの「FormResizeプロシージャ」を呼び出し、ユーザーがフォームのサイズを変更できる処置をします。詳細は図7-4で説明します。
13行目は「UserForm_Resizeイベントプロシージャ」を手動で呼び出しています。通常「UserForm_Resize」は「フォームのサイズを変更した」ら発生するイベントですが、手動で呼び出すことによりその中に記載してあるコードを実行することが出来ます。
今回「UserForm_Resize」の中に記載したコード内容は、図6-8の如く「WebBrowserコントロールをフォーム一杯に表示する」ことになります。

15~17行目は「WebBrowserの準備待ちをしているコードです。15行目のDo~Loopの条件には「WebBrowser1.Busy = True」と「WebBrowser1.ReadyState <> 4」の2つが使われています。

まず「WebBrowser1.Busy」は、図6-2の内容を示しています。
ブラウザのBusy値の推移
 値 説明
 True 新しいドキュメントを読み込んでいる
 False それ以外
図6-2

また「WebBrowser1.ReadyState」は、図6-3の内容を示しています。
ブラウザのReadyState値の推移
定数 値 説明
READYSTATE_UNINITIALIZED 0 デフォルト値。未完了状態
READYSTATE_LOADING 1 ページのロード中状態
READYSTATE_LOADED 2 ページのロード完了状態。ただし操作不可能状態
READYSTATE_INTERACTIVE 3 ページの操作可能状態
READYSTATE_COMPLETE 4 ページの全データ読み込み完了状態
図6-3

この2つを「Or」で繋いでいますので、どちらかがTrueの時は式全体がTrueになるためDo~Loopが続きます。つまり「WebBrowser1.Busy = False」「WebBrowser1.ReadyState = 4」と両方とも「一番下の状態になるまでDo~Loopが続く」事になります。
また16行目のDoEventsは、制御をO/S側に渡しているコードで、Excelが固まってしまうのを防止しています。

今回は3行目でページを与えていないので、「Busy」と「ReadyState」の状態がどうなるかを調べてみました。
すると、新しいドキュメントを読ませているわけでは無いのでBusy は最初から「False」の状態なのですが、ReadyStateの方はデフォルト値から完了状態(値4)へ順に移行してるようでした。

なお、この処理法は、IE(Internet Explorer)等のブラウザからページ内容を取得する時によく使われる「ページ読み取り待ち」のコードで、今回もこの処置をしないで実行すると「ページ書込み時(図6-4)にエラー」が発生します。

つぎに19行目で現在開いているシートを判断し、20・22行目で「WebStartプロシージャ」を呼び出します。
「WebStart(図6-4)」は「HTML文を読み込み、WebBrowserに書き込む」役目を持っていますので、「どの内容を読み込むか」を引数として受け渡しをします。
例えば、開いているシートがSheet2であるなら「システムその①を使っている」と判断し、それに対応したHelp内容は「html-01」であるため、20行目では引数に「html-01」を指定して「WebStart」を呼び出しているのです。
今回のサンプルファイルでは、Sheet2とSheet3の2画面しかシステムでは使っていない事にしましたので、「システムその②」はElseの「html-02」になります。

6-2.HTMLデータを読み込み、WebBrowserに書き込み

図6-1の20・22行目から呼び出されるのが、図6-4のWebStartプロシージャです。
引数として、ワークシート上に貼り付けたテキストボックスの名前を受取ります。
  1. '========== ⇩③ HTML文の読み込みとWebBrowserへの書込み ================
  2. Private Sub WebStart(str As String)
  3.  Dim HtmlStr As String
  4.  HtmlStr = Sheet1.Shapes(str).TextEffect.Text
  5.  WebBrowser1.Document.body.InnerHtml = HtmlStr
  6.  WebBrowser1.Document.parentWindow.scrollTo 0, 0
  7. End Sub
図6-4

29行目はテキストボックスからのHTML文の読み取りです。
テキストボックスの名前は引数strですので、テキストボックス自体へのアクセスは「Sheet1.Shapes(str)」となります。
そのテキストボックスのTextEffect(図形のテキストの書式設定プロパティ)のText(テキスト内容)で、記載してある内容を取得することが出来ます。

30行目は、29行目で取得したHTMLコードをWebBrowserに代入する部分です。
WebBrowserに表示されるHTMLコード(Bodyタグに挟まれたHTMLコード。但しBodyタグは含まず)は「WebBrowser1.Document.body.InnerHtml」に収まっています。

なお「InnerHtml」と似たプロパティとして「OuterHtml」が存在します。調べると「Bodyタグを含むHTMLコード」と説明されているのですが、「Bodyタグを含めたHTMLコードをOuterHtmlに代入」しようとすると、図6-5の様に「実行時エラー600」が発生してしまいます。
HTMLコードをOuterHTMLに代入するとエラー発生
図6-5

31行目は、Help画面を先頭に持ってくるコードです。Help画面がダイアログよりも大きな場合には、図6-6のように縦・横にスクロールバーが現れます。
Help画面が大きな場合は縦横スクロールバーが現れる
図6-6

そのスクロールバーをズラした状態でHelp画面を変更(ダイアログの上のボタンを押す)すると、そのままズレた状態で新たなHelp画面が表示されてしまいます。
それを31行目の「WebBrowser1.Document.parentWindow.scrollTo」で「スクロールの位置を設定」します。設定値は「水平軸上のピクセル値 , 垂直軸上のピクセル値」であるため、「0,0」とすることで「先頭が表示」されます。
ちなみに、スクロールバーが出ない画面の場合には、画面は当然ズレません。

6-3.ダイアログ上のボタン操作イベント

ダイアログの上に配置したCommandButtonを押した時のイベントが、図6-7になります。
  1. '========== ⇩④ ページ移動ボタンでの動作 ================
  2. Private Sub CommandButton1_Click()   '←Help-01へ移動
  3.  Call WebStart("html-01")
  4. End Sub
  5. Private Sub CommandButton2_Click()   '←Help-02へ移動
  6.  Call WebStart("html-02")
  7. End Sub
図6-7

HTML読み込み・書込みのWebStartプロシージャ(図6-4)は、引数として「HTMLコードを記したテキストボックス名」を受け取って処理します。図6-7の各ボタンも、そのテキストボックス名を引数にしてWebStartプロシージャを呼び出します。

ボタンの内「Help①」となっているCommandButton1は、テキストボックス「html-01」の内容を表示するものですので、html-01を引数に渡しています。一方「Help②」は「html-02」を引数に渡します。

6-4.フォームサイズの変更イベント

ユーザーがダイアログの枠をドラッグしサイズを変更した時に、図6-8のResizeイベントが発生します。なお、このイベントはマウスを離した時ではなく、サイズを変更している最中ずっと発生しています。
  1. '========== ⇩⑤ フォームサイズ変更イベント ================
  2. Private Sub UserForm_Resize()
  3.  Me.WebBrowser1.Width = Me.InsideWidth
  4.  Me.WebBrowser1.Height = Me.InsideHeight - Me.WebBrowser1.Top
  5. End Sub
図6-8

イベントコードの説明の前に、フォームの幅と高さについて説明します。(図6-9)
フォームの外側サイズと内側サイズ
図6-9

フォームに関わらず、コントロール等は最も外側のサイズを「Width」「Height」プロパティとして持っています。但しExcelのグラフ及びフォームには「InsideWidth」「InsideHeight」プロパティもあり、フォームに於いては「配置したコントロールが表示される範囲」を示しています。
図6-9を見て分かる通り、枠の太さは「(Width - InsideWidth )/2」という事になりますが、この値はPCの環境で異なるようです。同様に「Height - InsideHeight 」は「枠太さ+タイトル高さ」ですが、これも環境で値が変わります。

44行目は、WebBrowserの幅をフォームの表示幅(InsideWidth)と同一にしています。
尚、Initializeイベント(図6-1)の4行目で「WebBrowser1.Left = 0」としている事で、フォームとズレること無く「フォームの内側一杯にWebBrowserを表示」する事が出来るのです。
また45行目はWebBrowserの高さを、上部の2つのCommandButtonの下(WebBrowser配置時の上下位置)から、フォームの一番下まで一杯に表示させています。

ここで、Resizeイベントはフォームのサイズを変更している間ずっと発生していますので、ユーザーがフォームサイズを変更するに伴って、フォーム内のWebBrowserコントロールも追いついてサイズ変更されるため、フォームとWebBrowserコントロールの分離感無く動作することになります。

7.標準モジュールのコード

7-1.定数宣言、API参照宣言

標準モジュールの先頭で宣言するのが図7-1です。今回は全て「ユーザーによるフォーム変形のための宣言」です。
  1. '========== ⇩⑥ 定数・参照宣言(Module1) ================
  2. '// Win32API用定数
  3.  Private Const GWL_STYLE As Long = -16         '// ウィンドウスタイル
  4.   'Private Const WS_MAXIMIZEBOX As Long = &H10000    '//最大化ボタンを持つウィンドウ
  5.   'Private Const WS_MINIMIZEBOX As Long = &H20000    '//最小化ボタンを持つウィンドウ
  6.  Private Const WS_THICKFRAME As Long = &H40000     '//サイズ変更境界を持つウィンドウ
  7. '// Win32API参照宣言
  8.  Private Declare Function GetActiveWindow Lib "user32" () As Long
  9.  Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal Hwnd As Long, ByVal nIndex As Long) As Long
  10.  Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal Hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
  11.  Private Declare Function DrawMenuBar Lib "user32" (ByVal Hwnd As Long) As Long
図7-1

今回、フォームのサイズをユーザーが変更できるようにしています。Excelの標準状態ではサイズ変更は不可ですので、Win32APIの中から以下の4つの機能を使って実現させます。
 アクティブウィンドウを取得(54行目)
 ウィンドウに関しての情報を取得(55行目)
 ウィンドウに関しての情報を設定(56行目)
 ウィンドウのメニューバー外枠を再描画(57行目)

55・56行目の「GetWindowLong」・「SetWindowLong」で取得・設定する「ウィンドウ情報」の中で、今回必要となるのは「ウィンドウのスタイル」です。ですので第二引数には図7-2の内、「GWL_STYLE(値:-16)」を使いますので、49行目で定数として設定しています。
GetWindowLong・SetWindowLongで取得・設定するデータ
定数 値 取得される属性
GWL_WNDPROC-4ウィンドウプロシージャのアドレス
(直接ウィンドウプロシージャを呼び出すには CallWindowProc関数を使う)
GWL_HINSTANCE-6アプリケーションのインスタンスハンドル
GWL_HWNDPARENT-8親ウィンドウのハンドル
GWL_STYLE-16ウィンドウスタイル
GWL_EXSTYLE-20拡張ウィンドウスタイル
GWL_USERDATA-21ウィンドウに関連付けられたアプリケーション定義の32ビット値
GWL_ID-12ウィンドウ ID
図7-2

なお、他のExcelサイトでは49行目を「・・・= (-16)」とカッコで囲むことが多いのですが、この理由は「user32.dllの中では(-16)と定義しているから」としています。しかし、他言語サイトではカッコ有りのサンプルコードは見当たらず、またExcel中ではカッコ有無で動作に違いはありません。
user32.dll定義については調べ切れませんでしたが、サンプルファイルでは「カッコ無し」にしました。

また「ウィンドウのスタイル」には、図7-3の様に多くのスタイル項目があり、今回必要なのは「サイズ変更境界を持つウィンドウ」ですのでNo.15の値:0x00040000(=&H40000)を52行目で定数設定しています。
ウィンドウスタイル一覧
ウィンドウスタイル意味
1WS_OVERLAPPED,
WS_TILED
0x00000000オーバーラップウィンドウを作成します。
オーバーラップウィンドウはタイトルと枠を持ちます。
2WS_POPUP0x80000000ポップアップウィンドウを作成します。
このスタイルは、WS_CHILD スタイルと一緒には使えません。
3WS_CHILD,
WS_CHILDWINDOW
0x40000000子ウィンドウを作成します。
このスタイルは、WS_POPUP スタイルと一緒には使えません。
4WS_MINIMIZE,
WS_ICONIC
0x20000000ウィンドウを最小化の状態で作成します。
5WS_VISIBLE0x10000000可視状態のウィンドウを作成します。
6WS_DISABLED0x08000000無効 (使用不能) なウィンドウを作成します。
無効なウィンドウは、有効にするまで、
ユーザーからの入力を受け取りません。
7WS_CLIPSIBLINGS0x04000000兄弟関係にある子ウィンドウをクリップします。
8WS_CLIPCHILDREN0x02000000親ウィンドウ内部を描画するときに、
子ウィンドウが占める領域を除外します。
このスタイルは、親ウィンドウを作成するときに使います。
9WS_CAPTION0x00C00000タイトルバーを持つウィンドウを作成します。
(WS_BORDER | WS_DLGFRAME)
10WS_BORDER0x00800000境界線を持つウィンドウを作成します。
11WS_DLGFRAME0x00400000ダイアログボックスで一般的に使われるスタイルの
境界を持つウィンドウを作成します。
12WS_VSCROLL0x00200000垂直スクロールバーを持つウィンドウを作成します。
13WS_HSCROLL0x00100000水平スクロールバーを持つウィンドウを作成します。
14WS_SYSMENU0x00080000タイトルバー上にウィンドウメニューボックスを持つ
ウィンドウを作成します。
15WS_THICKFRAME,
WS_SIZEBOX
0x00040000サイズ変更境界を持つウィンドウを作成します。
16WS_GROUP0x00020000コントロールグループの最初のコントロールを指定します。
このコントロールから、次に WS_GROUP スタイルのコントロールがくる
までに定義されたコントロールが、コントロールグループになります。
17WS_TABSTOP0x00010000ユーザーが[Tab]キーを押すと入力フォーカスを受け取る
コントロールを指定します。[Tab]キーを押すと、WS_TABSTOP
スタイルを持つ次のコントロールに、入力フォーカスが移動します。
18WS_MINIMIZEBOX0x00020000最小化ボタンを持つウィンドウを作成します。
WS_SYSMENU スタイルも指定する必要があります。
拡張スタイルに WS_EX_CONTEXTHELP を指定することはできません。
19WS_MAXIMIZEBOX0x00010000最大化ボタンを持つウィンドウを作成します。
WS_SYSMENU スタイルも指定する必要があります。
拡張スタイルに WS_EX_CONTEXTHELP を指定することはできません。
20WS_OVERLAPPED WINDOW0x00CF0000WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU |
WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX に等しい。
21WS_POPUP WINDOW0x80880000WS_POPUP | WS_BORDER | WS_SYSMENU に等しい。
図7-3

なお、ウィンドウの最大化・最小化も利用する場合は、No.18、No.19の値を使用しますので、コメントアウトしてある図7-1の50・51行目を生かし、且つ図7-4の148行目を生かして下さい(147行目は不要になりますので、代わりにコメントアウトして下さい)。

7-2.フォームのリサイズ可能手続き

フォームを起動する際に、図6-1の12行目より呼び出されるのが図7-4のFormResizeプロシージャです。ここではフォームのリサイズを可能にする手続きをします。
  1. '========== ⇩⑦ フォームのリサイズ可能手続き(Module1) =============
  2. Public Sub FormResize()
  3.  Dim Hwnd As Long     '// ウィンドウハンドル
  4.  Dim Style As Long     '// ウィンドウスタイル
  5.  Hwnd = GetActiveWindow()   '// ウィンドウハンドルの取得
  6.  Style = GetWindowLong(Hwnd, GWL_STYLE)   '// ウィンドウスタイルの取得
  7.  Style = Style Or WS_THICKFRAME   '// ウィンドウのスタイルにウィンドウサイズ可変を追加
  8.   'style = style Or WS_THICKFRAME Or WS_MINIMIZEBOX Or WS_MAXIMIZEBOX  
  9.                    '↑ +最小ボタン+最大ボタンも追加の時
  10.  Call SetWindowLong(Hwnd, GWL_STYLE, Style)   '// ウィンドウのスタイルを再セットする
  11.  Call DrawMenuBar(Hwnd)             '//メニューバー外枠を再描画する
  12. End Sub
図7-4

63行目では「GetActiveWindow」を実行し、現在Activeになっているウィンドウの番号を取得します。このプロシージャは、フォームのActivateイベントの中から呼ばれていますので、フォーム自身がActiveなウィンドウとなります。
64行目では、そのフォームの情報を取得します。情報の種類は「GWL_STYLE」で指定していますので、「ウィンドウのスタイル」を得ることになり、そのデータは変数Styleに代入されます。

対象となっているウィンドウ内に「どのスタイルが組み込まれているか」は、変数Styleの値を計算すれば分かる事になっています。値は、図7-3の表の中で「組み込まれている項目の値を足したもの」だからです。但し、図7-3以外にもコントロール固有のスタイル値も存在するため、人力での計算は大変です。
そのため現状の分析は止めて、自分の「必要な項目を追加」するのが66行目になります。

66行目は「足し算」ではなく「Or」を使ってつないでいます。これについて、図7-3のウィンドウスタイルを簡易的にA~Eで置き換えた図7-5で説明します。
各スタイル値の組合せ計算(足し算法)
図7-5

各スタイルの値は左上の枠内に記載してある通り、項目ごとに1桁ずつズレた値となっています。なお、分かり易いように2進法で表示してあります。
一番左側の①はA・B・Dのスタイルが含まれた値(オリジナル値とします)に相当し、この計算はA・B・Dの値を足し算すれば求まります。

次に、そのオリジナルにスタイルEを追加することを考えます。(②)
オリジナルにはスタイルEが含まれていないので、普通に足し算すればA+B+D+Eの値は求まります。
しかし、もしオリジナルにスタイルEが既に含まれていたとしたら、どうなるでしょう(③)
普通に足し算をすると、桁が繰り上がってしまい思っていた値とは異なる値になります。③では「A+B+D+E」のはずが「A+B+C」になってしまうのです。これでは困ります。

ということで、足し算ではなく「Or を使った計算」の方法を図7-6で確認してみます。

各スタイル値の組合せ計算(Or法)
図7-6

「Or の計算」は、If文で使用するOrと同じです。「If A Or B」とあった場合、「AかBのどちらかがTrueだったらTrue」ですね。2進数での計算の場合「1がTrue 、0がFalse」ですので、その計算結果は図7-6の一番左の様な結果になります。
この計算法だと、先程の「スタイルEが含まれている値に、更にスタイルEを追加」しても、 図7-6の③のように計算結果が崩れることは無くなります。ちなみに、元の値にスタイルEが含まれていなくても、②のように大丈夫です。

このように「仕様有無を1つの値で表す場合、仕様追加にはOr」を使う事で正しい結果が得られます。
ちなみに仕様削除する際は「Orで追加後、引き算」が必要です。これは「値に削除する項目値が入っていない」場合に、間違った値になる事を防ぐためです。

67行目はコメントアウトしてありますが、フォームのサイズ変更のみで無く「最大化」「最小化」も可能にしたい場合には、「WS_MINIMIZEBOX 」と「WS_MAXIMIZEBOX 」も「Or」でつないでスタイルを追加してください。
尚、WS_MINIMIZEBOX等は値を事前に設定しておかなければ使えませんので、図7-1の50~51行目を生かしてから使用して下さい。

69行目では、サイズ可変のスタイルを追加した変数Styleを使って、ウィンドウ(フォーム)のスタイルを設定します。

また70行目ではメニューバー外枠を再描画していますが、フォーム右上のマークが変わらない「サイズ可変」のみならず、マークの変わる「最大化・最小化」を追加した時でも、見た目の変化は確認できませんでした。
理論的には必要と思いますので今回は「DrawMenuBar」を呼び出していますが、他サイトのサンプルコードでは無視しているところも多いようです。

7-3.ヘルプの起動

今回のシステムの場合、ユーザーが「F1キー」を押した時に呼び出されるプロシージャが図7-7です。
この「Help_Changeプロシージャ」が呼び出されるのは、Workbook_Openイベント(図8-1)で、「F1キーが押された場合にHelp_Changeプロシージャを呼び出す」ように登録されているからです。
  1. '========== ⇩⑧ ヘルプの起動(Module1) ================
  2. Public Sub Help_Change()
  3.  Dim Ans As Integer
  4.  Ans = MsgBox("アプリのHelpを開きますか(はい)" & vbCrLf & _
  5.          "標準のHelpを開きますか(いいえ)" & vbCrLf & _
  6.          "キャンセルしますか(キャンセル)", 3)
  7.  Select Case Ans
  8.   Case 6
  9.    UserForm1.Show
  10.   Case 7
  11.    Application.Help
  12.   Case 2
  13.  End Select
  14. End Sub
図7-7

75~77行目は1つの繋がったコードで、MsgBoxを呼び出しています。
MsgBox関数の構文は「MsgBox(prompt[, buttons] [, title] [, helpfile, context])」となっています。第一引数(メッセージ内容)のみを設定すると第二引数のボタンの種類は既定値の「0」となりますので、[OK]ボタンのみが表示されます。
反対に、第二引数の「buttons」に以下の値を設定することで様々なボタン表示が可能です。

尚、第二引数にはボタンの種類以外にも、アイコン種類や標準ボタンの設定の値も準備されています。
MsgBox関数の第二引数のボタンの種類一覧
定数 値 内容
vbOKOnly0[OK] ボタンのみを表示(既定値)
vbOKCancel1[OK] ボタンと [キャンセル] ボタンを表示
vbAbortRetryIgnore2[中止]、[再試行]、および [無視] の 3 つのボタンを表示
vbYesNoCancel3[はい]、[いいえ]、および [キャンセル] の 3 つのボタンを表示
vbYesNo4[はい] ボタンと [いいえ] ボタンを表示
vbRetryCancel5[再試行] ボタンと [キャンセル] ボタンを表示
図7-8

今回、ユーザーがF1ボタンを押した理由を以下の様に考えました。
 ①:ユーザーがアプリとしてのHelpを表示したいと考えてF1を押した
 ②:ExcelとしてのHelpを表示したいと考えてF1を押した
 ③:間違えて押してしまった
となると3種類の分岐が必要となり、図7-8の値で言うと「2」か「3」となりますが、この中から選ぶのであればボタンの文字から「3」しか無いと考え、77行目の第二引数は「3」にしました。
尚、フォームを使ってダイアログを作り、もっとカッコ良く分岐をさせるのも良いと思います。

そのMsgBoxでユーザーが操作(ボタンを押す)した結果は、MsgBox関数の戻り値として返ってきます。その戻り値は図7-9となります。
定数 値 内容
vbOK1[OK]
vbCancel2[キャンセル]
vbAbort3[中止]
vbRetry4[再試行]
vbIgnore5[無視]
vbYes6[はい]
vbNo7[いいえ]
図7-9(MsgBox関数のボタンの戻り値一覧)

MsgBox関数の第二引数に「3」を指示しましたので、ボタンの種類としては「はい、いいえ、キャンセル」になりますので、戻り値としては「6、7、2」のどれかになります。
この戻り値は75行目で変数Ansに代入され、MsgBoxとしては閉じられます。

その戻り値が代入された変数Ansを78~85行目のSelect Case文で分岐します。
80行目は「6=はい」でシステムHelpを起動させるため、UserForm1を立ち上げます。
82行目は「7=いいえ」でExcelのHelpを起動させるため、「Application.Help」でApplication =ExcelのHelpを呼び出します。
「2=キャンセル」の時は何もしない(MsgBoxを閉じただけ)ので、84行目は空欄です。なお「Case 2」のコード自体を削除してしまってもOKです。

8.ワークブックモジュール

本Book(=アプリ)を開いた時にF1キーにマクロを割り当て、閉じた時に解除するのが図8-1になります。
  1. '========== ⇩⑨ F1キーのマクロ割り当て・解除(Workbook) ================
  2. Private Sub Workbook_Open()
  3.  Application.OnKey "{F1}", "Help_Change"
  4. End Sub
  5. Private Sub Workbook_BeforeClose(Cancel As Boolean)
  6.  Application.OnKey "{F1}"
  7. End Sub
図8-1

マクロ割り当ては「Workbook_Open」イベント内で行います。
Application.OnKeyメソッドの第一引数に「押すキーを示す文字列」を指定します。キー毎のコードは図8-2のようになっています。
なお、PCによっては存在しないキーも含まれています。
Application.OnKeyメソッドに渡すキーコード一覧
キーコード
BackSpace{BACKSPACE} または {BS}
Break{BREAK}
CapsLock{CAPSLOCK}
Clear{CLEAR}
Delete または Del{DELETE} または {DEL}
終了{END}
Enter (テンキー){ENTER}
Enter~ (ティルダ)
キーコード
Esc{ ESCAPE} または {ESC}
ヘルプ{HELP}
ホーム{HOME}
Ins{INSERT}
NumLock{NUMLOCK}
PageDown{PGDN}
PageUp{PGUP}
Return{RETURN}
キーコード
Scroll Lock{SCROLL LOCK}
Tab{TAB}
{DOWN}
{LEFT}
{RIGHT}
{UP}
F1 ~ F15{F1} から {F15}
  
図8-2

また今回は使っていませんが、図8-2のキーと一緒に「Shift・Ctrl・Altキーを押す時」には、図8-3の記号を先頭に追加します。
キーコード
Shift+ (正符号)
Ctrl^ (カレット)
Alt% (パーセント記号)
図8-3(OnKeyのキーコードと同時に押すShiftキー等のコード)

今回はHelpを表す「F1キー」ですので「{F1}」となり、89行目ではこれを文字列の「"(ダブルクォーテーション)」で囲み、第一引数としています。
第二引数には、図7-7の「Help_Change」プロシージャを指定します。こちらも文字列として渡しますので「"(ダブルクォーテーション)」で囲みます。尚、呼び出すプロシージャに引数が必要となる場合の設定方法については「再計算されたか否かのチェック法」を参照下さい。

94行目ではアプリ終了(=Bookを閉じる)のときに、マクロ割り当てをしたF1キーを元の機能に戻すために「Application.OnKey "{F1}"」と設定します。
この最後の処置をしない(処理失敗も含めて)と、Excelで他の作業をしている際に、キー操作が思い通りにならなくなりますのでご注意下さい。


9.最後に

今回Excelを作っている途中で、図6-4の30行目を実行する際に、図9-1のエラーが極たまに発生していました。
HTMLデータ挿入時のエラー
図9-1

もしかしたら、HTMLデータを代入する先を「Document.body.InnerHtml」では無く、当初「Document.ActiveElement.InnerHtml」に代入していたからかもしれませんが、最後まで発生する条件等が把握できませんでした。これを使われる皆さんにもしかしたらご迷惑をお掛けするかもしれません。先に謝っておきます。
1つだけ分かっているのは「HTMLデータ(Bodyタグの内側)を代入する前に入っている内容が、Headタグを含む内容のHTMLデータ」ということです。どういうタイミングでInnerHtmlの中にHeadタグを含むデータが入ってしまうのかは分かりません。また、ダイアログを初めてLoadした時に発生している様な感じでした。

今回は、空文字をNavigateしてHTMLデータを直接代入するような手法が祟ってエラーが出たのかもしれませんが、HTMLとExcelをつなぐWebBrowserコントロールをうまく使うことで、使い勝手が良くなる気がします。これからも色々探っていくつもりです。


Book内で完結するフォーム表示のHelp画面(it-034.xlsm)

セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。