2020/08/04

流れる文字列




1.背景

ユーザーに注意を促したり重要な点を説明したりする最強の手段は、MsgBox等を表示させることです。しかしユーザー側にとっては、いちいちボタンをクリックしなければならず、分かっている人にとってはいい迷惑です。
一定時間で消えてくれるダイアログも作ることは可能ですが、分かっている人にはやはり邪魔です。
次に良くやる手段としては、システムのあちこちに「説明文を貼り付ける」方法です。しかしこの方法はあまり目立たず、文字を大きくしたり色を付けたり枠線を太くしたりして強調している方も多いと思います。

今回は、そんな説明文を「流れる文字」として表示する方法について説明します。
人の目は、静止してるものより「動いているもの」に注意が行くと思うので、貼り付けた文字よりは読んでもらえると思います。

2.概要

流れる文字の「流れる方法」と「表示する場所」から、図2-1のように4種類を考えました。
文字画像を動かす文字列を切り取り、部分表示する
フォーム上

ワークシート上


図2-1

「文字画像を動かす」方式は「1つの文字がジワジワと出現し、文字が流れた後ジワジワと消えていく」という街で良く見るタイプです。指定範囲の右端から現れ、左端で消えます
一方「文字列を切り取り、部分表示する」方式は「文字単位で出現・消える」タイプで、いかにもExcelという感じです。文字には全角・半角がありますし、フォントにより文字サイズも異なりますので、指定範囲の幅が文字の整数倍になるとは限りません。ですので指定範囲の途中から現れ、左端で消える形になります。

なお実際に動いている様子は、一番下のサンプルファイルのSheet1でボタンを押すことで確認できます。
また、Excelは並列処理はできませんので、1つずつ実行して下さい。

以下、4種のプログラムを1つずつ紹介します。

3.①フォーム上で文字画像を動かす

3-1.フォーム上のコントロール配置

フォーム上の流れる文字を表示する位置に「フレーム」を配置します。
「流れる文字列」用のコントロールは、ただ1つです。文字列を書き込むLabelコントロールは、マクロで生成します。
文字のフォントサイズはフレームの高さに比例させていますので、フレームを必要な高さに設定して下さい。
フレームをフォーム上に配置
図3-1

文字を流れる様に表示させているのはDo~Loop処理で行っています。今回はフォーム右上にボタンを設置し、フォームの終了+文字を流す処理を停止(Endステートメント)させています。
実際のアプリで使用する際にマクロ停止が難しい場合は、Do~Loopを停止させるコード(図3-9を参照)が必要となります。

3-2.フォームモジュールのコード

3-2-1.フォーム起動時の初期化

サンプルファイルでは、Sheet1の「①Form1」ボタンに登録されたマクロを経由し「UserForm1.Show」でフォームが起動されます。
  1. '========== ⇩① フォームの初期化(Initializeイベント) ====================
  2. Private Sub UserForm_Initialize()
  3.  Me.Frame1.Caption = ""
  4.  Me.CommandButton1.Caption = "Stop"
  5. End Sub
  6. '========== ⇩② フォームの初期化(Activateイベント) ====================
  7. Private Sub UserForm_Activate()
  8.  Dim Str1 As String
  9.  Str1 = "こんにちは さようなら 1234567890 ABCDE"
  10.  Call Flowing_Form_1(Me.Frame1, Str1)
  11. End Sub
図3-2

フォームが初めてLoadされる際に発生するイベントがInitializeイベントで、図3-2の2~5行目になります。
3行目は、フォーム上に配置したFrameの文字(Caption)を消すことで「枠線のみ」にし、「文字列の窓」っぽくしています。
また4行目は「フォームの停止・ループ処理の停止」を行うボタンの文字を「Stop」に変更しています。

フォームが表示された時に発生するイベントがActivateイベントで、図3-2の8~12行目になります。
10行目は、表示する文字列を設定しています。
11行目は、図3-3の「Flowing_Form_1」プロシージャを呼び出しています。引数として、「書き込むFrame枠のオブジェクト名」を第一引数に、10行目で設定した流れる「文字列」を第二引数に設定します。

3-2-2.Frame内で文字を流す処理

図3-2の11行目から呼び出されるのが図3-3です。第一引数で「書き込むFrame枠のオブジェクト名」を、第二引数で「流す文字列」を受け取ります。
  1. '========== ⇩③ 文字列処理 ====================
  2. Sub Flowing_Form_1(Frm As Object, Str As String)
  3.  Dim MyCtrl As Object
  4.  Dim MyTime As Single
  5.  Set MyCtrl = Frm.Controls.Add("Forms.Label.1", "F_Label", True)
  6.  With MyCtrl
  7.   .Caption = Str
  8.   .Font.Size = Frm.Height * 0.6
  9.   .WordWrap = False
  10.   .AutoSize = True
  11.   .Left = Frm.Width
  12.   .Top = (Frm.Height - .Height) / 2
  13.  End With
  14.  Do
  15.   MyCtrl.Left = MyCtrl.Left - 5
  16.   DoEvents: DoEvents
  17.   If MyCtrl.Left < -1 * MyCtrl.Width Then
  18.    MyCtrl.Left = Frm.Width
  19.   End If
  20.   mytime = Timer
  21.   Do While Timer - mytime < 0.2
  22.    DoEvents: DoEvents
  23.   Loop
  24.  Loop
  25. End Sub
図3-3

まず、今回手法の考え方を図3-4で説明します。
フレーム内でラベルを動かす構造
図3-4

フォーム上に配置したフレームの中に、ラベルを作ります。そのラベルには文字列が記入されます。
フレームの中に含まれたコントロールは「フレーム部分しか見えず、他は隠れて」見えます。丁度、フレームが窓の様な役目になります。
この状態でフレームの位置は固定し、中のラベルだけを動かして行けば、あたかも文字が流れているように見える というわけです。

18行目は、Frameコントロール内にLabelを動的作成しています。
Addメソッドの第一引数には作成するコントロールのプログラム識別IDを指定します。今回は文字列を書き込みますのでLabelを指定しますが、プログラム識別IDでは「Forms.Label.1」という文字列になります。
第二引数には作るコントロールの名前を指定します。省略可能な引数ですし、Addの戻り値(Labelオブジェクト)が代入される変数MyCtrlを使ってその後の処理をしていますので、本当は設定不要です。今回「F_Label」と指定したのは説明のためです。
第三引数は、オブジェクトの表示(True)・非表示(False)を指定します。Trueが既定値ですので、これも今回省略しても問題ありません。

18行目で作成したFrame内のLabelは、オブジェクトとしてMyCtrl変数に代入されます。そのLabelオブジェクトのプロパティ設定を20~25行目で行っています。

20行目は、流す文字列をLabelに書き込み、21行目では文字サイズをフレームの高さの60%に設定しています。これは「ワークシートのセルの高さと文字のサイズの比」が約60%であることから決めています。この値を60% → 100%にするとフレーム高さ一杯に文字が表示されます。

22行目は折り返しを禁止(WordWrap = False)し、23行目で文字列全体が表示される(AutoSize = True)ようにします。この設定をしないと、与えられた文字列が全て表示されない可能性があるため必須です。

24~25行目はラベルの位置の設定です。通常フォーム内に配置したラベルの位置はフォームの左上角を原点としていますが、フレーム内のラベルの場合はフレームの左上角を原点として位置決めします。
初期のラベル位置は、図3-5の通りフレームの右端位置に配置します。
ラベルの初期位置
図3-5

文字列を繰り返し動かす部分が、28~40行目のDo~Loop部分です。
まず29行目では、流す文字を記入したLabelの左右方向位置を、「5ポイント」ずつ移動しています。このポイント数は大きくすると文字列が早く動きます。
30行目の「DoEvents: DoEvents」は「DoEvents」を2回書いたのと同じ意味ですが、制御をO/Sに渡し、ユーザー側が操作できるようにしています。

32~34行目は、流す文字列の表示が終わってしまった時に、位置を戻すコードです。
32行目は、Labelの左上角の左右方向位置(MyCtrl.Left)が、Labelの幅(MyCtrl.Width)よりも小さい時、というIf文です。Excelの座標は左上角を原点として、左右方向なら右側が+になりますので、「MyCtrl.Width」には「-(マイナス)」が付いています。
つまり図3-6で分かる様に、Labelがフレームを外れたら(=流す文字列が完了したら)33行目を実行することになります。
ラベルの突き当り位置
図3-6

33行目は図3-6の通り、Labelの位置をフレームの右側に移動させています。これは24行目と同じコードですので初期位置にさせている事になります。

36~39行目は、時間調整です。
36行目の右辺の「Timer関数」は、午前0時からの経過秒数をSingle型で戻すもので、1秒以下の時間を得ることが出来ます。ですので変数mytimeには「現在時刻(含1秒以下)」が代入されます。
37行目はDo~Loopで、回している条件は「Timer - mytime < 0.2」です。これを図3-7で説明します。
Do Loop処理を抜ける時間
図3-7

例えば、1つの処理に0.06秒かかるとします。時間は左から右に進んで行きます。
36行目でTimer関数で時刻を計測し変数mytimeに代入していますが、これは図3-7の②に相当します。
Do~Loopに入り、37行目では都度Timer関数で時刻を計測します。その時刻から元の時刻mytimeを引くことで、36行目からの時間を得ることが出来ます。この時間を設定した「0.2秒」と比較をしています。
図3-7では、②~④までは0.2秒未満ですのでDo~Loopを抜けられません。⑤になると0.2秒を超えますのでDo~Loopを抜けて次の処理(29行目に戻る)に移行します。

これにより、29~34行目の処理は「0.2秒ごとに実行」されるのです。
なお、ここでは説明上「処理に0.06秒かかる」としましたが、実際には(私のPCでも)もう1桁以上は早いです。例えば30回まわっているとすると、Do~Loopで回っている時間は「0.2 ~ 0.2067秒」ですので、約3%の誤差ということになります
科学技術で3%は致命的ですが、流れる文字でしたら許される誤差と思われます。

またこの「0.2秒」の値を小さくすると「文字の流れはスムーズ」になりますが、その分マクロ側の「文字を動かす時間」が増えるため、アプリ側の使える時間が減るデメリットがあります。

3-2-3.文字を流す処理の停止

フォーム上のStopボタンを押すことで作動するプロシージャが図3-8です。
  1. '========== ⇩④ 停止処理 ====================
  2. Private Sub CommandButton1_Click()
  3.  Unload Me
  4.  End
  5. End Sub
図3-8

44行目でフォームを閉じます。今回は「Unload」を使っていますが、「Hide」で閉じてもOKです。但し「Unload」でも「Hide」でも、図3-3の28~40行目のDo~Loopは回り続けていますので、強制的に止める必要があります。
今回は45行目の「Endステートメント」でマクロを停止させていますが、アプリ上これが不可能な場合は、停止フラグを使ってDo~Loopを停止させる必要があります。
図3-9は、フォームのモジュール変数Stop_Flagを使って、マクロを停止させています。
  1. Dim Stop_Flag As Boolean       '←フォームモジュール変数として、先頭でフラグを宣言
  2. Sub Flowing_Form_1(Frm As Object, Str As String)
  3.  Dim MyCtrl As Object
  4.  Dim MyTime As Single
  5.  Set MyCtrl = Frm.Controls.Add("Forms.Label.1", "F_Label", True)
  6.  With MyCtrl
  7.   .Caption = Str
  8.   .Font.Size = Frm.Height * 0.6
  9.   .WordWrap = False
  10.   .AutoSize = True
  11.   .Left = Frm.Width
  12.   .Top = (Frm.Height - .Height) / 2
  13.  End With
  14.  Do
  15.   If Stop_Flag = True Then Exit Do    '←TrueだったらDo~Loopを抜ける
  16.   MyCtrl.Left = MyCtrl.Left - 5
  17.   DoEvents: DoEvents
  18.   If MyCtrl.Left < -1 * MyCtrl.Width Then
  19.    MyCtrl.Left = Frm.Width
  20.   End If
  21.   mytime = Timer
  22.   Do While Timer - mytime < 0.2
  23.    DoEvents: DoEvents
  24.   Loop
  25.  Loop
  26.  Stop_Flag = False         '←Do~Loopを抜け出たらFalseに戻す
  27. End Sub
  28. Private Sub CommandButton1_Click()
  29.  Unload Me
  30.  Stop_Flag = True          '←Stopボタンを押したらフラグが立つ(True)ようにする
  31.   'End                '←Endステートメントは削除
  32. End Sub
図3-9

4.②フォーム上で文字列を部分表示し流れるように見せる

4-1.フォーム上のコントロール配置

フォーム上に「ラベル(Label)」を図4-1の様に配置します。文字のフォントサイズはLabelコントロールの高さに比例させていますので、Labelを必要な高さに設定して下さい。
フォーム上にLabelを配置する
図4-1

文字を流れるように見せているのはDo~Loop処理を使用していますので、フォーム上にStopボタンを配置し、フォームの終了+Do~Loop処理の停止を行っています。

4-2.フォームモジュールのコード

4-2-1.フォーム起動時の初期化

サンプルファイルでは、Sheet1の「②Form2」ボタンに登録されたマクロを経由し「UserForm2.Show」でフォームが起動されます。
  1. '========== ⇩⑤ フォームの初期化(Initializeイベント) ====================
  2. Private Sub UserForm_Initialize()
  3.  Me.CommandButton1.Caption = "Stop"
  4. End Sub
  5. '========== ⇩⑥ フォームの初期化(Activateイベント) ====================
  6. Private Sub UserForm_Activate()
  7.  Dim Str1 As String
  8.  Str1 = "こんにちは さようなら 1234567890 ABCDE"
  9.  Call Flowing_Form_2(Me.Label1, Str1)
  10. End Sub
図4-2

フォームがLoadされる際に発生するイベントがInitializeイベントです。
88行目では、マクロ停止処理用のCommandButtonの表面文字に「Stop」を記入しています。

フォームが表示された時に発生するイベントがActivateイベントです。
94行目で、表示する文字列を設定しています。
95行目は、図4-3の「Flowing_Form_2」プロシージャを呼び出しています。引数として、「表示するLabelオブジェクト名」を第一引数に、94行目で設定した「表示する文字列」を第二引数に設定します。

4-2-2.Labelに文字列を流すように貼り付ける処理

図4-2の95行目から呼び出されるのが図4-3です。第一引数で「表示するLabelオブジェクト」を、第二引数で「表示する文字列」を受け取ります。
  1. '========== ⇩⑦ 文字列処理 ====================
  2. Sub Flowing_Form_2(Lab As Object, Str As String)
  3.  Dim Str_count As Long    '←Label内に表示する文字数
  4.  Dim MyTime As Single
  5.  Dim Spa As String       '←文字列の先頭に追加するスペース列
  6.  Dim i As Long         '←文字列から表示文字を切り出す先頭位置
  7.  Lab.Font.Size = Lab.Height * 0.6
  8.  Lab.WordWrap = False
  9.  Str_count = Lab.Width / Lab.Font.Size * 1.4
  10.  Spa = Application.WorksheetFunction.Rept(" ", Str_count)
  11.  Str = StrConv(Spa & Str, vbWide)
  12.  Lab.Caption = ""
  13.  i = 1
  14.  Do
  15.   Lab.Caption = Mid(Str, i, Str_count)
  16.   i = i + 1
  17.   DoEvents: DoEvents
  18.   If i > Len(Str) Then i = 1
  19.   MyTime = Timer
  20.   Do While Timer - MyTime < 0.5
  21.    DoEvents: DoEvents
  22.   Loop
  23.  Loop
  24. End Sub
図4-3

この「②フォーム上で文字列を部分表示し流れるように見せる」手法の考え方を図4-4で説明します。
文字列を部分表示し流れるように見せる方式の考え方
図4-4

まずフォーム上に配置されたLabelに文字が何文字入るかの計算をし、その値を「Str_count」とします。
表示する文字列の先頭に、Str_count分のスペースを結合しておきます。
その結合した文字列の先頭から、Str_count分の文字列を取り出します。ある秒数ごとにその先頭位置を移動していき(図4-4では右端の i=〇〇)、Labelの文字を書き換えていきます。これにより、文字が流れている様に見える、というカラクリです。

104~106行目では、「Labelに何文字入るか」を計算します。
まず104行目では、文字サイズをLabelコントロールの60%に設定します。これは図3-3の21行目と一緒ですが、「ワークシートのセル高さt文字のサイズの比」が約60%であることから決めてあります。この値を60%→100%にすると、Labelコントロール高さ一杯に文字が表示されます。
105行目は「文字列の折り返し」を無くす指示です。図3-3の22行目と同様の処理です。

106行目は「Labelに入る文字数」を数えています。
1つの文字の横幅は、文字そのものによっても異なりますし、フォントでも違います。また全角・半角もありますし、文字間の間隔も様々です。この文字の横幅を決める要素の中で、最も差が大きいのは全角・半角だと思いますので、今回は「全て全角に揃える」ことでバラツキを少しでも抑えるようにしました。(109行目のStrConv(文字列,vbWide) 処理)

全角に揃えた上で、Labelの横幅(Lab.Width)を文字の縦サイズ(Lab.Font.Size)で割った値を基準としました。これは「全角文字は、およそ真四角」という大雑把な計算方法を使っています。
最後の「x 1.4 」は調整係数で、文字を実際に流してみて調整しています。

この調整係数を大きくすると、文字がLabelコントロール一杯に出てくるようになりますが、大きすぎると先頭に結合するスペースの量が増えてしまい「なかなか文字が出てこない」現象が発生してしまいます。
反対に調整係数が少ないと、文字はすぐ出てきますが、Labelの右端が空いてしまい、文字が途中から出てくるイメージになります。

108行目は、文字列の先頭に結合するスペースを作っています。ワークシート関数のRept関数を使用して、スペースをLabelの幅分(=Str_count)繰り返して、変数Spaに代入しています。
スペースは、109行目で全て全角にしてしまうため、108行目では半角でも全角でもOKです。
109行目は、引数で与えられた文字列の先頭に108行目で作成したスペースを結合した上で、StrConv関数で全角に揃えています。

110行目は、Labelに既定値として設定されている「Label〇〇」という文字がフォーム起動時に見えてしまうため、「Caption = ""」と文字を消しています。この処理はフォームのActivateイベント内で行っても良いと思います。

111行目の「i = 1」の「i」は文字列を切り出す開始位置ですので、最左端の1としています。
これを「i = 2」(同時に117行目も、i = 2 に変更)に変更すると、フォームを起動した直後に最初の1文字が現れ、最後の文字が消えた直後に最初の文字が現れるようになります。ここは、趣味の問題なので設定自由です。

112~123行目はDo~Loopで処理停止するまで繰り返し実行する部分です。
113行目は、109行目で作った文字列(Labelの幅分のスペース+引数で与えられた文字列)から、先頭をi番目、文字数はLabel幅分の文字数 を抜き出して、LabelのCaptionに代入しています。
文字を表示したら、次の表示のために114行目で「i = i + 1 」と、表示先頭位置を移動させています。

117行目では、文字列が一番最後の文字位置まで達したかを調べ、達していた(i < Len(Str))ら、i を先頭(=1)に戻しています。
この「<」を「<=」にしてしまうと、最後の文字が突然消えてしまい違和感が生じますので注意が必要です。

119~122行目は、待ちの時間調整です。①のUserForm1に於いては、この部分は「0.2」に設定したのですが、②の今回は「文字がジワジワ消えていくわけでは無い」ので、動くスピードを①と同じくらいに合わせる為に「0.5」にしました。
尚、②の文字列の一部を切り取ってLabelに貼り付ける方法では、文字が流れるスピードを調整するところは120行目しかありません。
この部分の詳細については、図3-3の36~40行目の説明を参照して下さい。

121行目、及び115行目の「DoEvents: DoEvents」は、O/S側に制御を渡している部分です。これを設定しないと「文字は流れても、それ以外の処理が出来ない」「ボタンも押せない」という、いわゆる無限ループになりますので、充分注意して下さい。

4-2-3.フォームの停止処理

フォーム上のStopボタンを押すことで作動するプロシージャが図4-5です。
  1. '========== ⇩⑧ 停止処理 ====================
  2. Private Sub CommandButton1_Click()
  3.  Unload Me
  4.  End
  5. End Sub
図4-5

127行目でフォームを閉じます。今回は「Unload」を使っていますが、「Hide」で閉じてもOKです。
但し「Unload」でも「Hide」でも、図4-3のDo~Loopは回り続けていますので、強制的に止める必要があります。
今回は128行目の「Endステートメント」でマクロを停止させていますが、アプリ上これが不可能な場合は、停止フラグを使ってDo~Loopを停止させる必要があります。その方法については、図3-9を参照下さい。

5.③ワークシート上で文字画像を動かす

5-1.ワークシート側の準備

流れる文字を表示するセルを決め、セル幅・セル高さを整えます。表示される文字サイズは「セル設定の文字サイズでは無く」、セルの高さで決めています。
また、連続する複数セルでの表示も可です。またMerge(セル結合)も可ですが、図5-1の中の表示範囲設定(変数R)にはMerge先頭セルではなく結合している全てのセル範囲を指定して下さい。

5-2.標準モジュールのコード(Module1)

5-2-1.流れる文字列の呼出し

サンプルファイルでの「ボタンを押したら」呼び出されるのが、図5-1です。ここから、流れる文字列のプロシージャ(図5-2)を呼び出します。実際のシステムでは同様の機能を盛り込んで下さい。
なお終了する時は、フォームの場合と同様にEndステートメントを使用するか、または図3-9のような停止フラグの使用が必要です。
  1. '========== ⇩⑨ 流れる文字列の呼出し(Module1) ===================
  2. Sub start1()
  3.  Dim Str As String
  4.  Dim R As Range
  5.  Str = "こんにちは さようなら 1234567890 ABCDE"
  6.  Set R = Sheet1.Range("b2")    '←表示範囲設定
  7.  Call Flowing_Cell_1(R, Str)
  8. End Sub
図5-1

135行目は、流す文字列を設定します。半角は半角として、全角は全角として表示されます。
136行目は、流す文字列の表示場所を設定します。連続している複数セル範囲も可です。Merge(セル結合)範囲も設定可ですが、ここでの範囲設定には全てのセル範囲を指定が必要です。

137行目は、その2項目(表示場所、流す文字列)を引数として、図5-2の「Flowing_Cell_1」プロシージャを呼出します。

5-2-2.流れる文字列作成(Module1)

図5-1の137行目から呼び出されるのが、図5-2です。第一引数として表示するセル範囲、第二引数として表示する文字列を受取ります。
  1. '========== ⇩⑩ 流れる文字列作成(Module1) ===================
  2. Sub Flowing_Cell_1(R As Range, Str As String)
  3.  Dim MyCtrl As Object     '←表示する文字列を入れるテキストボックス、及びそれを図形に変更した後のオブジェクト
  4.  Dim sh As Shape        '←シート上のShapeオブジェクト(以前の文字図形を削除するため)
  5.  Dim Range_Width As Single   '←セル幅(Shape.widthの型に合わせSingle型で宣言)
  6.  Dim Sh_Width As Single    '←図形文字の幅(セルに合わせて拡大・縮小する前の値)
  7.  Dim Start_Pos As Long     '←図形を切り出す開始位置
  8.  Dim MyTime As Single     '←待ち時間のスタート時刻
  9.  Dim Ratio As Single      '←図形の拡大・縮小率
  10.  Application.ScreenUpdating = False
  11.   For Each sh In Sheet1.Shapes
  12.    If sh.Name = "Flowing_Box" Then sh.Delete
  13.   Next sh
  14.   Set MyCtrl = Sheet1.Shapes.AddLabel (msoTextOrientationHorizontal, R.Left, R.Top, 100, 100)
  15.   MyCtrl.TextFrame.Characters.Text = Str
  16.   MyCtrl.TextFrame.AutoSize = True
  17.   MyCtrl.Copy
  18.   MyCtrl.Delete
  19.   Set MyCtrl = Sheet1.Pictures.Paste
  20.   MyCtrl.Name = "Flowing_Box"
  21.   Sh_Width = MyCtrl.Width      'サイズ変更前に幅を変数に代入
  22.   Range_Width = R.Width
  23.   MyCtrl.Height = R.Height     'ここで縦横比保持でサイズ変更
  24.   Ratio = MyCtrl.Width / Sh_Width
  25.   MyCtrl.Top = R.Top
  26.   MyCtrl.Left = R.Item(R.Count).Offset(0, 1).Left
  27.   Sheet1.Shapes(MyCtrl.Name) .PictureFormat.CropLeft = -1 * Range_Width / Ratio
  28.   Sheet1.Shapes(MyCtrl.Name) .PictureFormat.CropRight = Sh_Width   '長い文章だと誤差大
  29.  Application.ScreenUpdating = True
  30.  Start_Pos = 0
  31.  Do
  32.   On Error Resume Next
  33.    Sheet1.Shapes(MyCtrl.Name) .PictureFormat.Crop.PictureOffsetX = (Range_Width + Sh_Width * Ratio) / 2 - Start_Pos
  34.   If Not Err.Number = 0 Then GoTo S_stop
  35.    Start_Pos = Start_Pos + 10
  36.    If Start_Pos >= (Range_Width + Sh_Width * Ratio) Then
  37.     Start_Pos = 0
  38.    End If
  39. S_stop:
  40.   On Error GoTo 0
  41.   DoEvents: DoEvents
  42.   MyTime = Timer
  43.   Do While Timer - MyTime < 0.5
  44.    DoEvents: DoEvents
  45.   Loop
  46.  Loop
  47. End Sub
図5-2

まず、図形(Shape)のトリミングについて図5-3で説明します。
図形のトリミング
図5-3

通常のトリミングは、図5-3の左のように図形を内側に絞り込む形でトリミングします。トリミングした後は、絞り込まれたものが図形として操作できます。そのトリミング量は、上下左右の端からの「Crop〇〇」というプロパティ値で指定します。
この「Crop〇〇」プロパティ値は、PictureFormatオブジェクトに対して設定するものです。PictureFormatオブジェクトは、図形オブジェクトのPictureFormatプロパティを使用して得ます。具体的には、図形が「Sheet1.Shapes(1)」であった場合、「Sheet1.Shapes(1).PictureFormat」と「図形オブジェクト + .(ピリオド) + PictureFormat」のように指定します。

なお、トリミング量である「Crop〇〇」プロパティ値はプラス側だけではありません。例えば図5-3の右側のように「CropLeftをマイナス、CropRightをプラスの値に設定」すれば、最初の文字列を左側にオフセットした部分が表示されることになります。
つまり「図形の無い部分に図形を作る」みたいなことが可能で、これが今回の流れる文字に使っている方法です。

またトリミングは、図の元の大きさに基づいて計算されますので、図5-4のようにサイズ変更した後の図形をトリミングする場合は、元図のサイズに立ち返って値を決める必要があります。
トリミングは元画像の大きさに基づき計算
図5-4

尚、サイズ変更した後に「図をコピー」→「図で貼付け」をすれば、貼り付けた図の変形倍率は100%になりますので、見た目のポイント数でトリミング指示を出来ることになります。

149行目の「Application.ScreenUpdating = False」から173行目の「Application.ScreenUpdating = True」の間で、図形の作成・削除、及び図形のサイズ変更・移動等を行っています。ユーザーには不要な情報であり、パラパラと画面が動きますので画面更新をストップさせています。

150~152は、Sheet1上にある図形(Shape)を1つずつ調べ、「Flowing_Box」という名前だったら削除しています。この名前は161行目で自ら付けている名前ですので、「以前実行した流れる文字列の図形を削除」するという意味になります。
ここで、誰にでも思いつく様な名前を設定してしまうと、ユーザーが作った図形を削除してしまうことにもなりますので、注意が必要です。

154行目は、Sheet1上に「横書きのLabel図形」を作っています。引数の第2~第5引数は、何を設定してもOKです。とりあえず文字表示を行うセル近くに作成していますが、意味はありません。
154行目で作ったLabel図形は、オブジェクトとして変数MyCtrlに代入されており、155行目ではそのLabelの文字として引数Strである「流す文字列」を設定します。
156行目の「AutoSize = True」の設定で、文字サイズ(Excelのユーザー設定フォントサイズ)に合ったサイズに修正されます。またこの時に文字折り返しも無くなります。このAutoSize処置をしますので154行目でLabelを作る際、適当な大きさで許されるわけです。

158行目は、そのLabel図形をコピーします。
159行目は、Label図形を削除します。削除しても、158行目で行ったコピー内容はクリップボード上に残っています。
160行目は、そのクリップボードの内容(=Labelの文字列)を「画像として」貼り付けます。画像ですので戻り値はPicture型としてオブジェクトがMyCtrlに代入されます。
161行目で、その画像に名前「Flowing_Box」を付けます。この名前は、次に実行する時に削除対象(151行目)になります。

163行目は、図形としての文字列の幅を取得し、変数Sh_Widthに代入しています。この段階では、文字列の図形は元図のサイズですので、このSh_Width値をトリミングに使用します。 164行目は、表示セルの幅を取得し変数Range_Widthに代入します。この値も、図5-5のように「CropLeft」の値として使用したいのですが、このままでは使えません。
トリミングと文字列図形の位置関係
図5-5

この時点での文字列図形は、この後(166行目)の処理で表示セル高さにサイズ変更されてしまうからです。(図5-6)
「トリミング量は元図の大きさで計算」しますので、Sh_Widthの方は大丈夫なのですが、Range_Widthをそのまま使ってしまうと、図5-6では一点鎖線で示すように、正しくトリミング出来ないことになります。
文字列図形のサイズ変更
図5-6

ですので、サイズ変更をした後に再度文字列図形の幅(MyCtrl.Width)を取得し、元図の幅(163行目で取得)との比を変数Ratioとします(167行目)。
そして、文字列図形のトリミングの「CropLeft」には、164行目のRange_WidthとRatioを使って「サイズ変更前相当の表示セル幅」に換算した後、その値を指定するようにしています。

166行目では、表示セル高さに合わせて「文字列図形のサイズ変更」をしています。
初期状態では「図形の縦横比保持(LockAspectRatio = msoTrue)」になっているため、比例して横幅も変更されます。
安心のためには、変形前に「LockAspectRatio = msoTrue」のコードを実行しておいた方が良いかもしれません。

167行目は前述しましたが、元図形の幅(Sh_Width)とサイズ変更後の幅(MyCtrl.Width)の比を取り、変数Ratioに代入します。このRatio値は、トリミング時のみではなく、図5-5の「Crop.PictureOffsetX」(トリミング範囲~文字列図形の距離)の計算にも使用します。

168行目は、文字列図形の上下方向を表示セルに合わせます。
169行目は、左右方向を表示セルの一つ右のセルに初期値として合わせます。これは、176行目以降のDo~Loopで文字列図形を少しずつ移動していくのですが、指定した文字列の先頭が表示セルの右端から順次現れるようにするため、初期状態としては文字列先頭を右隣のセルとの境目に置いているのです。
また、右隣のセルに配置するのであれば「R.Offset(0, 1).Left」で良いのですが、もし「表示セルを複数セルに指定」された場合は「R」は先頭のセル位置(一番左のセル)を指しますので、表示がおかしくなります。
ですので、複数セル指定された場合でも対応できるように「R.Item(R.Count).Offset(0, 1).Left」と、指示された表示セルの一番右「R.Item(R.Count)」を基準として、その右隣のセルを指定するようにしました。

171行目は「CropLeft」を設定します。設定量としては「マイナスの表示セル幅」ですが、元画像の大きさを基に考える必要があります。セル幅(Range_Width)を画像サイズ変更比(変数Ratio)で割った値が「元画像に於けるセル幅相当の寸法」になりますので、「Range_Width / Ratio 」を設定します。また「マイナス」にするため、「-1」を掛けます。

172行目は「CropRight」の設定です。これも元画像を基に考えます。163行目では「画像のサイズ変更前に」画像の幅を取得していますので、その値を直接CropRightに設定することで「トリミングした結果は、表示セル範囲」となります。

ちなみに、今回は「CropTop」と「CropBottom」については設定していませんので、154~156行目で作った「テキストボックス」のフォントサイズとテキストボックスの高さの関係のまま、となっています。ですので、流れる文字列の上下が空いたような感じになります。
もし隙間を詰めたいのであれば、テキストボックス段階でフォントサイズと高さを取得し上下方向もトリミングをするか、または、166行目でサイズ変更をする際に大きめの画像にし、貼り付ける位置を調整することが必要になります。

173行目では流れる文字列を見せるために、149行目の「Application.ScreenUpdating = False」での画面更新停止を解除しています。
また174行目では、以降で文字列画像を動かして流れる文字相当にするための移動量「Start_Pos」の初期化を行っています。尚、このStart_Posの変数宣言(145行目)時で既にゼロが初期値になっていますので、このコードは実行しなくても影響ありませんが、明示的に記載しました。

ここまでで、形としては図5-5のような「流れる文字が始まる直前」の状態になりました。
176~193行目のDo~Loopで、文字列画像を少しずつ動かして「流れる文字列」に見えるようにしていきます。

177・179・185・186行目の「エラー処理」については、後でまとめて説明します。まずは、文字列を流すための処理について説明します。

178行目は、文字列画像の「画像本体の位置とトリミング位置とのズレ量」を「Crop.PictureOffsetX」で指定します。
その関係は図5-7で示す通り、「トリミング中心」と「画像本体の中心」のズレ量を、「サイズ変更後の画像基準」で表します。トリミング量の時とは異なる基準ですので注意が必要です。

文字列画像の開始時は、図5-7の左側の状態です。「Crop.PictureOffsetX」は、「トリミング中心」と「画像本体の中心」の距離ですので、「(Range_Width + Sh_Width * Ratio) / 2」となります。
文字図形・トリミング枠とPictureOffsetXの値の関係
図5-7

その初期状態から徐々に文字列図形を左に動かすには、図5-7の右図の中の「X」の値を徐々に増やしていけば良いことがわかります。この「X」が、178行目の一番右の「Start_Pos」になります。
Start_Posは、174行目でゼロと初期化していますので、Do~Loopの中で徐々に増えていくように181行目で「Start_Pos = Start_Pos + 10」としています。この「10」が増分で、値を大きくすれば早く動くように見えます。

どんどん「Start_Pos」(= X )を大きくしていくと、その内表示する文字列図形がトリミング範囲から見えなくなります。図5-8のような状態です。
トリミング枠から文字図形が外れたことを検知する方法
図5-8

外れたか否かは、図5-8の「Range_Width」と「Sh_Width * Ratio」を足した値より、X(=Start_Pos)が大きくなったか否かを調べれば良いことが分かります。
ですので182行目では If文で判断し、文字列図形が外れたら「Start_Pos」値を初期状態(174行目と同じ状態)に戻してあげれば、次に178行目を実行する時には、文字列図形は図5-7の左側の状態になることになります。

189~192行目は時間待ち調整で、ここでは「0.5秒ごと」に文字列図形を移動させています。この値を小さくすれば「流れがスムーズに」また「流れが速く」なりますが、逆にユーザー側が使える時間が減ります。

さて「エラー処理」についてです。
この文字列図形の移動は、178行目の「Sheet1.Shapes(MyCtrl.Name).PictureFormat.Crop」オブジェクトの「PictureOffsetX」プロパティ値を変更させ続けることで実現しています。
この処理をしている(=文字が流れている)最中に、どこかのセルを「編集モード」にすると、図5-9のようなエラーが発生します。コメントを読むと「Cropオブジェクトの使用が拒否された(=文字列図形の移動を邪魔された)」ためのようです。
オブジェクトエラー1
図5-9

エラーの回避方法としては「On Error GoTo 〇〇」や「On Error Resume Next」がありますが、「オブジェクトにアクセスするときは、On Error Resume Next を使用(Microsoftのサイトより)」しなければならないため、177行目で「On Error Resume Next」を使用しています。
実際に試してみても、確かに「On Error GoTo 〇〇」ではエラー回避が出来ませんでした。

では、エラーが出た(=ユーザーがセル操作をした)場合に「流れる文字をどうしておくか」を考える必要があります。2つの方法があると思い、1つ目は「流れる表示を停止し、ユーザー操作が完了したら再開する」方法、2つ目は「文字は裏で動かしておき、ユーザー操作が完了したら再開する」方法です。
2つ目の方法は「操作完了した瞬間、流れる文字の位置が大きく変わる」ことから「ユーザーには異様に見える」と判断し、1つ目の方法を取ることにしました。

そのためには、エラーが出ている間に「Start_Posを進めない」ことが必要になるため、178行目で発生したエラーの番号を179行目で調べ、ゼロ以外(=エラーが出ている状態)だったら「S_stop」に飛ぶようにしました。
こうすることで、181行目の「Start_Pos = Start_Pos + 10」が実行されず「Start_Posを進まない」ことになります。
なお182~184行目は、Start_Posが動かない限り実行する必要はありませんので、その先に「S_stop」を設けています。

186行目で「On Error GoTo 0」としているのは、178行目のエラー発生時に「他の行でのエラーを捕まえられなくならないように」毎回(=0.5秒ごと)エラー解除させています。

なお、今回は「Crop.PictureOffsetX」の値を使って文字列図形のみを動かす方法を取りましたが、他の方法もあります。例えば、文字列図形を動かす部分だけを記述すれば、図5-10のようになります。
  1. Start_Pos = 0
  2. Do
  3.  With Sheet1.Shapes(MyCtrl.Name)
  4.   .IncrementLeft -10
  5.   .PictureFormat.CropLeft = (-1 * Range_Width / Ratio) + Start_Pos + 10
  6.   .PictureFormat.CropRight = Sh_Width - Start_Pos - 10
  7.  End With
  8.  Start_Pos = Start_Pos + 10
  9. Loop
図5-10

この手法は図5-11のように「IncrementLeft」を使って図形として全体を動かし、動かしたあとでトリミングをやり直すという方法です。
文字図形ごと動かす方法
図5-11

どちらの方法でも、見た目はあまり変わりません。理解し易いやり方が良いと思います。


6.④ワークシート上で文字列を部分表示し流れるように見せる

この手法は、②の「フォーム上で文字列を部分表示」する方法のワークシート版です。似たような部分が多くありますので、そちらも参照下さい。

6-1.ワークシート側の準備

流れる文字を表示するセルを決め、セル幅・セル高さを整えます。表示される文字サイズはセルのフォントサイズに従いますので、フォントサイズを先に決めてからセル高さを調整した方が良いかもしれません。
また、複数のセルに表示させる場合は、セル結合(Merge)してください。またセル結合した全てのセル範囲を指定して下さい。

6-2.標準モジュールのコード(Module2)

6-2-1.流れる文字列の呼出し

サンプルファイルでの「ボタンを押したら」呼び出されるのが、図6-1です。ここから、流れる文字列のプロシージャ(図6-2)を呼び出します。実際のシステムでは同様の機能を盛り込んで下さい。
なお終了する時は、フォームの場合と同様にEndステートメントを使用するか、または図3-9のような停止フラグの使用が必要です。
  1. '========== ⇩⑪ 流れる文字列の呼出し(Module2) ===================
  2. Sub start2()
  3.  Dim Str As String
  4.  Dim R As Range
  5.  Str = "こんにちは さようなら 1234567890 ABCDE"
  6.  Set R = Sheet1.Range("b4")
  7.  Call Flowing_Cell_2(R, Str)
  8. End Sub
図6-1

209行目は、流す文字列を設定します。半角文字で設定しても、文字は全て全角に修正されて表示されます。これはセル幅に入る文字数の計算を簡単にするためです。
210行目は、流す文字列の表示場所を設定します。Merge(セル結合)範囲も設定可ですが、全てのセル範囲を指定が必要です。
211行目は、その2項目(表示場所、流す文字列)を引数として、図6-2の「Flowing_Cell_2」プロシージャを呼出します。

6-2-2.流れる文字列作成(Module2)

図6-1の211行目から呼び出されるのが、図6-2です。第一引数として表示するセル範囲、第二引数として表示する文字列を受取ります。
  1. '========== ⇩⑫ 流れる文字列作成(Module2) ===================
  2. Sub Flowing_Cell_2(R As Range, Str As String)
  3.  Dim Str_count As Long      '←表示セルに収まる文字数
  4.  Dim Sp As String        '←文字列の先頭に結合するスペース文字
  5.  Dim MyTime As Single      '←待ち時間のスタート時刻
  6.  Dim Start_Pos As Long      '←文字列を切り出す開始位置
  7.  Str_count = R.Width / R.Font.Size * 0.9
  8.  Sp = Application.WorksheetFunction.Rept(" ", Str_count)
  9.  Str = StrConv(Sp & Str, vbWide)
  10.  Start_Pos = 1
  11.  Do
  12.   On Error Resume Next
  13.   R.Value = "'" & Mid(Str, Start_Pos, Str_count)
  14.   If Not Err.Number = 0 Then GoTo S_stop
  15.   Start_Pos = Start_Pos + 1
  16.   If Start_Pos > Len(Str) Then Start_Pos = 1
  17. S_stop:
  18.   On Error GoTo 0
  19.   DoEvents: DoEvents
  20.   MyTime = Timer
  21.   Do While Timer - MyTime < 0.5
  22.    DoEvents: DoEvents
  23.   Loop
  24.  Loop
  25. End Sub
図6-2

220行目では、表示セルに入る文字数を計算しています。文字そのものやフォントにより文字幅は異なりますので、概算になります。
求め方としては、表示セル幅を表示セルのフォントサイズで割った値を使っています。式の最後の「x0.9」は調整代で、実際に文字を流してみて調整しています。
代入する先の変数「Str_count」は215行目でLong型として宣言していますので、220行目の右辺で小数点が出ても切り捨てられる事になります。

221行目は、ワークシート関数の「Rept」を使って、スペース(今回の場合あとで全角変換するので、半角でも全角でもOK)を「表示セルに入る文字数分(Str_count)」結合し、変数Spに代入します。
222行目では、そのSp(スペース x 表示セルに入る文字数)を引数Str(表示する文字列)の先頭に結合し、全てを全角に変換しています。(図6-3)
表示する文字列を全角に変換
図6-3

なお、全てを全角に変換している理由は、セル内表示の幅をできるだけ一定にするためです。

223行目で、文字列を切り出す位置(変数Start_Pos)の初期化をします。
そして、224~239行目のDo~Loopの中で、文字列の切り出し位置を順次変えていきます。

Do~Loop中の、225・227・231・232行目のエラー処理は、③のエラー処理と全く同じです。表示するセル位置(引数R )はRangeオブジェクトですので、前述の通り「On Error Goto S_stop」は使えません。ですので225行目では「On Error Resume Next」を使う必要があります。
尚、Rangeオブジェクトで発生するエラーは図5-9とは少し異なり、図6-4となるようです。
オブジェクトエラー2
図6-4

226行目は「Mid(Str, Start_Pos, Str_count)」で、文字列StrをStart_Pos番目の文字からStr_count個分切り取ります。それを表示セルの値(R.value)に代入します。
なお、切り出した文字列が「12345」の様な全角の数字だけだった場合、セルにそのまま貼り付けると「数値と判断されて半角の数値」になってしまいます。今回は「全角の文字列として表示」しようとしているのですから、全角として表示されるように先頭に「'(シングルクォーテーション)」を付けています。

229行目は、切り出し位置を1つ移動するために「Start_Pos = Start_Pos + 1」とStart_Posの値を1つ増やしています。
230行目は、文字列の切り出し位置であるStart_Posが(先頭のスペースを含めた)表示文字列の文字数を超えたら、表示は完了です。折り返して再び先頭からの文字表示を繰り返すべく、「Start_Pos=1」と再設定しています。

235~238行目は、他方法と同様の時間調整です。

7.最後に

4種を紹介しましたが、実用に耐えられるかと聞かれれば、どれも苦しいかもしれません。
しかしその中でフォーム上で文字を流す方式(①と②)は、ウラでDo~Loopが動いているにも関わらずマウス形状に変化が無く違和感が無いので、これを元に工夫すると良いものになる気がします。

一方、ワークシートのセルに文字を流す方(③と④)は、マウスの形状が目まぐるしく変わり「ウラでDo~Loop」が動いているのが結構気になりました。
年中流し続けるのではなく、OnTimeで「時々、文字が流れる」とか、「しばらく入力していないと文字が流れる」くらいが、ユーザーの作業の邪魔をせず、実用的なのかもしれません。


流れる文字列(it-035.xlsm)

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