2020/04/06
2020/04/13改訂 (図2-23の下側に追記。及び添付ファイルも更新しました)

画像を直接シートに貼り付ける




1.背景と概要

Excelを使って書類を作る方は非常に多いと思います。その書類の分かり易さ・説得力・正確性を向上させる1つの方法として、画像(写真)を貼り付ける手法があります。
しかしシートに画像を貼り付けるには図1-1の様に、「挿入タブ」→「図」→「画像」→「エクスプローラ形式でファイル選択」する必要があります。しかも、貼り付いた画像のサイズは元の画像のサイズであり、適切なサイズに伸縮するのに手間取ることも多いかと思います。

図1-1

そこで今回、図1-2のようにエクスプローラからファイルをドラッグ&ドロップすれば、画像がシートに貼り付くマクロを作成しましたので紹介します。

図1-2

このマクロの取扱説明書を書くとすると、以下のようになります。
<操作方法>
1)ダイアログを事前に起動しておく。
2)ワークシート上で「画像を貼り付ける位置とサイズ」を範囲選択する。
3)エクスプローラ等から画像ファイルをダイアログに「ドラッグ&ドロップ」する。(複数同時も可)
4)選択セル範囲に画像が貼り付く。

<仕様>
1)アクティブにした選択範囲に画像が貼られます。
2)貼付け方法として「①セルにフィット」と「②元画像の縦横比を維持」が選べます。起動時は②がONです。
  ①セルフィット:元画像の縦横比を崩してでも選択範囲の形にフィットさせます。
  ②元画像比維持:元画像の縦横比はそのままに、選択範囲内に収まるように貼り付けます。

<対応している画像ファイル種類>
「JPG(JPEG)」「GIF」「BMP」「ICO」「RLE」「WMF」「EMF」「PNG]「TIFF(TIF)」
尚、マルチページTIFFは先頭頁が貼られます。またGIFは動く画像として貼られる訳ではありません。
(また、私は画像についてあまり詳しく無く、「ICO」「RLE」「WMF」「EMF」については動作未確認です。)

回転させたJPGファイルで不具合が発見されました

本項が完成した後で分かったのですが、JPGファイルを回転(90度または270度)させたファイルは、WidthとHeightが逆に記録されています(縦方向がWidth、横方向がHeight)。
よって、90度・270度回転させたJPGファイルを今回のマクロで「画像比維持」でシートに貼り付けると、画像比が崩れます。
(180度は問題無し。また、BMP・Tiff・PNGは大丈夫そうです。)

LoadPictureでは回転角が取得できないため、簡単な改造での解決は難しそうです。このことを理解した上で参照・利用を御願いします。
私も他の方法を考えてみて解決できるものが出来ましたら、また紹介いたします。



2.プログラム

今回のマクロは、全てフォームモジュールに記載しています。標準マクロにも1行記載しますが、これはフォームを起動させるだけのコードです。

2-1.フォームの作成

2-1-1.ListViewについて

エクスプローラからファイルをドラッグ&ドロップする先として、今回は「ListViewコントロール」を使用します。
ListViewの本来の使用法は、図2-1の様にファイル名などをリスト状に表示して選択できるようにするものですが、ListViewコントロールには「ドロップされたイベント(OLEDragDrop)」があり、ドロップファイルのパス名が受け取れますので、その機能を今回利用します。

図2-1

ちなみに、「OLEDragDrop」の「OLE」とは「他のアプリケーションとの間でデータを共有するための仕組み」という意味で、今回の場合ですと「エクスプローラとExcelの間で」「ファイルをドラッグ&ドロップすることをきっかけに」「ファイル内容を共有する」ということになります。

では、そのListViewコントロールを配置してみましょう。しかし標準のツールボックスにはListViewコントロールはありません。
そこで、ListViewコントロールを図2-2の方法でツールボックスに追加します。

1)ツールボックス上でマウスを右クリックし、「その他のコントロール」を選択します。
2)リストの中から「Microsoft ListView Control version 6.0」を選択(先頭にレ点を付ける)しOKボタンを押します。
 (version 6.0 は変わる可能性があります。)
3)ツールボックスにListViewマーク「」が追加されます。


図2-2

「ListView」が見当たらない、という方に

ネット上には「ListViewは64bit版Officeでは使えない」との書き込みがあります。主はExcel 2013のようで、私のPC(Win10 64bit x Office2016 64bit)では全く問題無く使えますし、ExcelのバージョンやPCの環境等で可否が分かれるようです。
また、問題無く作る事が出来た今回ファイル「画像を直接シートに貼り付ける(it-023.xlsm)」も、あなたのPCでは動かない場合も考えられます。

もし「ListViewが作れない」「動かない」等の場合は、ドラッグ&ドロップの受け口として、「ListView」コントロールの代わりに「WebBrowser」コントロールを使用する方法があります。
「WebBrowser」が64bitで動かないという話は無さそうなので、ListViewがダメな方は試してみる価値はありそうです。
但しListViewでは複数ファイルの同時ドロップが可能でしたが、WebBrowserは単一ファイルに限られる事は仕様ダウンになりますのでご了承下さい。

WebBrowser については、第3項「ListView の代わりに WebBrowser を使う方法について」で説明をします。また、サンプルファイル「画像を直接シートに貼り付ける(it-023.xlsm)」には、UserForm2にWebBrowserで作成したものを付けてあります。
(WebBrowser 仕様(UserForm2側)を使う場合は、標準モジュールの起動プロシージャを書き換えて下さい。)



2-1-2.コントロールの配置

ツールボックスにListViewが追加できましたら、クリックしてフォーム内に図2-3の様にListViewを作成します。

図2-3

ListViewの大きさは自由ですが、マウスでファイルをドロップするだけですので、それほど大きくなくても良いと思います。
他にはOptionButtonを2個配置し「セルフィット」と「画像比維持」の切り替えが出来るようにしています。

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

2-2-1.Initializeイベント

フォームを起動する時にフォームの初期設定をするのがInitializeイベントプロシージャ(図2-4)です。
  1. '========== ⇩① フォームの初期設定 ===============
  2. Private Sub UserForm_Initialize()
  3.  Me.ListView1.OLEDropMode = ccOLEDropManual   ’←ListViewのドロップモードを手動にする(ドロップが可能になる)
  4.  Me.Caption = "画像トンネル"
  5.  Me.OptionButton2.Value = True           ’←画像比維持を標準にする
  6. End Sub
図2-4

3行目の「ListView1.OLEDropMode = ccOLEDropManual」は、図2-3で作成・配置したListViewのプロパティである「OLEDropMode」を設定しています。
既定値の「ccOLEDropNone」ではファイルをドロップしても反応しませんので、「ccOLEDropManual」へ設定をしています。

4行目は、フォームのタイトルを設定。5行目では、画像貼付けの方法(「セルフィット」or「画像比維持」)をどちらを既定にするかを設定しています。


2-2-2.ListViewのDropイベントプロシージャ

画像をListView枠に落とした時には、図2-6の「(オブジェクト名)_OLEDragDrop」というイベントが発生します。

画像ファイルをドロップしてから貼り付けるまでの、大雑把なプログラムの流れを図2-5に示します。
流れを理解してからコードを見ていただくと、少し分かり易くなるかと思います。

図2-5

  1. '========== ⇩② ファイルをドロップした時のイベントプロシージャ ========
  2. Private Sub ListView1_OLEDragDrop(Data As MSComctlLib.DataObject, Effect As Long, Button As Integer, Shift As Integer, x As Single, y As Single)
  3.  Dim FilePath As String    ’←ドロップしたファイルのPATH付ファイル名
  4.  Dim Pict_W As Long     ’←画像ファイルの幅
  5.  Dim Pict_H As Long     ’←画像ファイルの高さ
  6.  Dim Err_No As Long     ’←エラー番号を一時保存
  7.  Dim cnt As Long      ’←カウンタ変数(=一度にドロップしたファイル数)
  8.  For cnt = 1 To Data.Files.Count   ’←複数ファイルをドロップした際は、1つずつ処理
  9.   FilePath = Data.Files(cnt)    ’←ドロップしたファイルのファイル名を取得
  10.   Select Case UCase(Split(FilePath, ".")(UBound(Split(FilePath, "."), 1)))  ’←ファイル名の拡張子を調査
  11.    Case "JPG", "JPEG", "GIF", "BMP", "ICO", "RLE", "WMF", "EMF"  ’←JPG,GIF,BMPなどの画像ファイルの場合
  12.     Dim PP As Object
  13.     On Error Resume Next
  14.      Set PP = LoadPicture(FilePath)    ’←ファイルを画像としてPP変数に取り込み
  15.      Pict_W = CLng(PP.Width * 0.0378)   ’←画像の幅を計算(ピクセル換算)
  16.      Pict_H = CLng(PP.Height * 0.0378)   ’←画像の高さを計算(ピクセル換算)
  17.      Err_No = Err
  18.     On Error GoTo 0      ’←エラー発生時はForを脱出しエラー処理完了出来ない為、事前に完了させる
  19.     If Not Err_No = 0 Or Pict_W = 0 Or Pict_H = 0 Then     ’←エラーが出たかサイズがゼロかの場合
  20.      MsgBox "画像ファイルでは無いか" _
  21.          & vbCrLf & "サイズ認識不可です"         ’←エラーコメントを出し
  22.      GoTo NextPicture                    ’←次のファイルの処理に移る
  23.     End If
  24.    Case "PNG"                    ’←PNGの場合
  25.     Call Png_WH(FilePath, Pict_W, Pict_H)    ’←ファイルの縦横を取得
  26.     If Pict_W = 0 Or Pict_H = 0 Then       ’←エラーが出ていれば
  27.      MsgBox "PNGファイルでは無いか" _
  28.          & vbCrLf & "サイズ認識不可です"    ’←コメントを出して
  29.      GoTo NextPicture              ’←次のファイルの処理に移る
  30.     End If
  31.    Case "TIF", "TIFF"               ’←Tiffの場合
  32.     Call TIF_WH(FilePath, Pict_W, Pict_H)   ’←ファイルの縦横を取得
  33.     If Pict_W = 0 Or Pict_H = 0 Then      ’←エラーが出ていれば
  34.      MsgBox "TIFFファイルでは無いか" _
  35.          & vbCrLf & "サイズ認識不可です"  ’←コメントを出して
  36.      GoTo NextPicture            ’←次のファイルの処理に移る
  37.     End If
  38.    Case Else
  39.     MsgBox "画像ファイルでは無いか" _
  40.         & vbCrLf & "未対応のファイルです"  ’←拡張子が画像では無いファイルの場合
  41.     GoTo NextPicture              ’←次のファイルの処理に移る
  42.   End Select
  43.   If Me.OptionButton1.Value = True Then  ’←セルフィットの場合
  44.    Pict_W = Selection.Width        ’←選択範囲幅を画像幅にする
  45.    Pict_H = Selection.Height       ’←選択範囲高さを画像高さにする
  46.   Else                   ’←画像比維持の場合
  47.    Select Case (Selection.Width / Selection.Height) / (Pict_W / Pict_H)   ’←貼付範囲と画像の縦横比を比較
  48.     Case Is >= 1              ’←選択範囲の方が横長の時
  49.      Pict_W = Selection.Height * (Pict_W / Pict_H)
  50.      Pict_H = Selection.Height
  51.     Case Is < 1               ’←選択範囲の方が縦長の時
  52.      Pict_H = Selection.Width * (Pict_H / Pict_W)
  53.      Pict_W = Selection.Width
  54.    End Select
  55.   End If
  56.   ActiveSheet.Shapes.AddPicture _
  57.       FileName:=FilePath, _
  58.       LinkToFile:=False, SaveWithDocument:=True, _
  59.       Left:=Selection.Left, Top:=Selection.Top, _
  60.       Width:=Pict_W, Height:=Pict_H    ’←画像貼り付け
  61.  NextPicture:                  ’←ファイル読み込み・サイズエラー時の飛び先
  62.  Next cnt
  63. End Sub
図2-6

2-2-2-1.宣言部・繰り返し部
イベント「ListView1_OLEDragDrop」には引数が5つ(「Data」「Effect」「Button」「Shift」「x」「y」)有りますが、ドロップされたファイル情報(ファイル名、及びファイル数)は、「Data」引数に収められています。

9~13行目はプロシージャ内で使用する変数宣言です。
「FilePath」は、ドロップしたファイルのファイル名(フルパスを含む)です。
「Pict_W」「Pict_H」は、ドロップした画像ファイルの幅と高さを代入する変数です。
「Err_No」は、エラー発生時にそのエラー番号を一時保存しておく変数です。
「cnt」は、15行目のForで使われており、一度にドロップしたファイル数をカウントしていきます。

15行目の「For cnt = 1 To Data.Files.Count」は、78行目の「Next cnt」までの間を繰り返し実施するものです。繰り返し回数は、一度にドロップしたファイル数(Data.Files.Count)となります。

2-2-2-2.ファイル名の取得
16行目の「FilePath = Data.Files(cnt)」の右辺は、ドロップしたファイル名を取り出しています。正確に書けば「Data.files.Item(cnt)」となります。
もし複数ファイルをドロップした際は、「Data.Files(1)」「Data.Files(2)」・・・と連続して格納されていますので、順に取り出していきます。
取り出したファイル名(String型)は、変数FilePathに代入し以降で使用しています。

2-2-2-3.拡張子での選別
18行目の「Select Case」で、ファイル種類毎の処理をするために振り分けを行っています。
その評価する式は「UCase(Split(FilePath, ".")(UBound(Split(FilePath, "."), 1)))」となっており、これはファイル名の拡張子部分になります。少し複雑なので、図2-7で説明します。


図2-7

図2-7は、例としてファイル「ABC.jpg」がドロップされた時の処理手順です。
まず16行目で、「パス名+ABC.jpg」は文字列として「FilePath」に代入されます。

18行目にある「Split(FilePath, ".")」の「Split関数」は、「パス名+ABC.jpg」を「.(ピリオド)」で分割し配列にする関数です。
通常のパス名+ファイル名でしたら、その中に存在する「.(ピリオド)」は一番後ろの拡張子の前に1つだけ存在するはずですので、「パス名+ABC」と「jpg」と分かれて配列に入れられます。

Splitで分割した配列は必ず添字ゼロから始まりますので、「ABC」が添字0、「jpg」が添字1 という事になります。
一方UBound関数は「配列の最大添字を求める」関数ですので、「UBound(Split(FilePath, "."), 1))」は1となります。
配列の各要素を取り出すには、配列の後ろにカッコ付きで添字を指定します。ですので、ファイル名の最後端に付いている拡張子「jpg」の文字列を取り出すには「Split(FilePath, ".")(1)」つまり「Split(FilePath, ".")(UBound(Split(FilePath, "."), 1))」とすれば良いことになります。

先頭の「UCase」は、指定した文字列を大文字に直す意味ですので、「jpg」が「JPG」と大文字になります。大文字に揃えることで、Case文の振り分けが単純化できます。
(揃えないと Case "jpg","jpG","jPg","jPG","Jpg","JpG","JPg","JPG" と全組み合わせを記述する事になってしまいます。)

なお、「パス名+ファイル名」の中に複数の「.(ピリオド)」が存在する場合もあります。例えば「 C:¥Users¥USER¥Pictures.New¥ABC.1.jpg 」の様な場合は、Split関数で作られる配列は図2-8のように0~3の4要素配列となります。しかし2要素が4要素になったとしても「UCase(Split(FilePath, ".")(UBound(Split(FilePath, "."), 1)))」で得られる値は、やはり「一番最後の拡張子」となることが分かると思います。


図2-8

つまり18行目は、「拡張子でファイル種類を判別」し処理を分けるSelect Case文になります。
分ける種類は、20行目の「"JPG", "JPEG", "GIF", "BMP", "ICO", "RLE", "WMF", "EMF"」、34行目の「PNG」、42行目の「"TIF", "TIFF"」、50行目の「それ以外」の4種です。

2-2-2-4.Excel標準関数での画像サイズ取得
まず「"JPG", "JPEG", "GIF", "BMP", "ICO", "RLE", "WMF", "EMF"」は、「Excel関数でサイズを取得できるファイル種」です。

21行目は、この画像種類のみで使用する変数の宣言で、画像を読み込むための変数PPです。
23行目では、その変数PPに画像を読み込んでいます。LoadPictureで読み込まれた画像はPicture型のオブジェクトですので、PPに代入するには Set を使用します。

24~25行目では、LoadPictureで読み込んだ画像(Picture型)の幅(Width)と高さ(Height)を取り出します。
Picture型でのWidthプロパティとHeightプロパティで得られる各長さ単位は、HIMETRIC単位と言って「0.01mm単位」での値となります。

この「0.01mm単位」を画像サイズの単位の「ピクセル」に換算するには、図2-9の関係から「HIMETRIC値に 0.0378 を乗ずる」計算をします。
また、ピクセルには小数点以下はありませんので、24~25行目ではCLngでLong型に型変換しています。


図2-9

なお、セル範囲のサイズ(57行目以降の「Selection.Width」「Selection.Height」)や、今回は使いませんが「シートに貼り付けた画像(Shape型)」のサイズ(ShapeオブジェクトのWidthプロパティやHeightプロパティ)の場合は「ポイント」として値が得られます。
ですので、RangeオブジェクトやShapeオブジェクトでの「ポイント → ピクセル」換算は、ポイントに「 96/72 」を乗じます。

ちなみに、24~25行目で代入される「画像の縦横サイズ」ですが、今回のプログラム中では直接ピクセル値は使わず「縦横の比」のみを使いますので、ピクセルに変換することは必須ではありません。
今回ピクセル変換したのは、34行目以降のPNG画像処理・42行目以降のTIFF画像処理で取得する画像サイズがピクセルですので、それに桁を合わせる為だけです。

2-2-2-5.画像取り込み・サイズ取得時のエラー処理
この23~25行目(ドロップされた画像の縦横サイズを取得する工程)を「On Error Resume Next」と「On Error GoTo 0」で囲っています。これは、23~25行目でエラーが発生した時のエラー処理です。

エラーの内容として、例えば「本当はTiffファイルなんだけど、拡張子をjpgに偽装した」ようなファイルが来た場合、LoadPictureで取り込もうとした段階(23行目)でエラーが発生しますので、プログラムが止まることを防止しているのです。

そして、26行目でエラー番号をErr_No変数に代入してから「On Error GoTo 0(27行目)」でエラー防止を解除してます。
これは、エラーが発生していた場合は「コメントを出し(29~30行目)」+「For Next を抜ける(31行目)」を実施した後、次のファイルの処理に移ってしまいます。しかし、次のファイル処理でもまだ「On Error Resume Next」の効力が効いてしまうため、異常が発生しても停止しません。
正確に言うと「16~21行目でエラーが出てもスルーしてしまう」ことになります。

通常では16~21行目でエラーが出ることは無いと思いますが、「On Error Resume Next」は想定済みのエラーに対して使用するものであり、何が起こるか分からない場所で使用してはいけません。
また、ドロップしたファイルが1つであったり複数ファイルでも最後のファイル処理であれば、マクロ終了と共に「On Error Resume Next」の効力は無くなり、次にイベントが発生した際の処理時には何の問題もありません。
しかし「On Error Resume Next」の後は必ず「On Error GoTo 0」で締める事は重要だと思います。

26行目でErr_No変数に代入されたエラー番号は、28行目の「If Not Err_No = 0」で評価し、0以外であれば29~31行目を実行します。
尚、「ファイルが壊れている等」の場合を想定して、28行目のIF文に「Or Pict_W = 0 Or Pict_H = 0」を追加しました。

この追加部分は、24~25行目で得られた画像サイズが「ゼロ」だったら「エラーとして処理」するという意味になりますが、どのような画像ファイルがサイズゼロと計算されるのか現時点では確認出来ていません。しかし、下の「PNG」「TIFF」の処理中にエラーが発生した場合は「サイズゼロが返される」ことが分かっているため、真似て設定する事にしました。

エラーが発生した場合は、29~30行目のコメントを出した後、31行目の「GoTo NextPicture」で78行目に移動し、次のファイルの処理に移ります(続きの処理ファイルが無ければ終了)。

2-2-2-6.PNG、TIFFファイルの画像サイズ取得
次に、拡張子が「PNG」の場合です。
図2-20のSubプロシージャ「Png_WH(ファイル名, 画像Width, 画像Height)」を呼び出して、画像ファイルのサイズを得ています。
計算結果を得るのですから「Function」では?と思われるかもしれません。もちろんFunctionで値を得ることもできますが、Subプロシージャでも得られるのです。

今回のプログラムを単純化したものが図2-10です。mainプロシージャの中からPng_WHプロシージャを呼び出す段階では、「FilePath」「Pict_W」「Pict_H」の各変数にはそれぞれ「△△△」「0」「0」が入っています。
呼び出したPng_WHプロシージャの中では、色々計算をした結果として「Pict_W」「Pict_H」の各変数を「▲▲」「■ ■」で書き換えています。
ですので、Png_WHプロシージャから制御が返ってきた時には「Pict_W」「Pict_H」に値が入っており、この値を使って処理を続けられる事になります。


図2-10

このような事になるのは、Subプロシージャを呼び出す時に、引数を「参照渡し(ByRef)」で渡してるためです。(何も指定せずに引数を渡す場合は参照渡しになります)
反対に「値渡し(ByVal)」で引数を渡すと、呼び出し前と呼び出し後で値は変わらないことになります。

この様にして得られた「Pict_W(画像の幅)」「Pict_H(画像の高さ)」を使い、その値がゼロ(=画像ファイルがエラー)であるならば(36行目)、37~38行目でコメントを出し、39行目で次のファイルの処理へ移動します。

42行目は「"TIF"」又は「"TIFF"」の場合の処理です。
PNGの場合と同様で、TIF_WH(ファイル名, 画像Width, 画像Height) プロシージャを呼び出し、得られた「Pict_W」「Pict_H」を使って44~48行目でエラーの処理をしています。

2-2-2-7.その他のファイル処理
50~53行目は「Case Else」ですので、拡張子が上記以外の場合の処理になります。
例えば「XLSX」等は画像でないのでダメな事は分かりますが、画像である「PDF」や「RAW」もその他の処理になります。
処理方法は、コメントを出し、次のファイルの処理に移動することは一緒です。

2-2-2-8.貼り付けるサイズを決める(セルフィット)
56行目では、フォームの中に配置したOptionButtonの状態を調べ、「セルフィット(OptionButton1を選択)」であれば57~58行目を実施。「画像比維持(OptionButton2を選択=既定値)」であれば61~70行目を実施します。
また、実際に画像を貼り付けるコードは、73~77行目の「ActiveSheet.Shapes.AddPicture」であり、その中の引数「Width:=Pict_W, Height:=Pict_H(77行目)」で貼り付ける幅(Width:=)と高さ(Height:=)を単位「ポイント」で指定します。

「Width:=」と「Height:=」に値を指定する際、それ専用の変数を設けても良いのですが、77行目の通り今回は「Pict_W」と「Pict_H」を使いました。
この「Pict_W」「Pict_H」変数は、ドロップした画像ファイルの「幅」と「高さ」ですが、直接ピクセル値を使用するわけではないので「ActiveSheet.Shapes.AddPicture」の引数へ値を与える変数を兼ねることにした訳です。


図2-11の左側写真は画像のサイズ、右側の青い部分は貼付け先である選択範囲のサイズを示しています。
なお、ここで言う「選択範囲」は、「最初に選択した範囲」としています。もし飛び地で選択した場合には、その内のItem(1) (=最初に選択した範囲)が「選択範囲」ということになります。


図2-11

まず「セルフィット」の場合は、図2-11の下側の様に「画像を変形させて、選択セルの大きさに合わせる」ので、「Pict_W(Width)」には選択範囲の幅(=Selection.Width)、「Pict_H(Height)」には選択範囲の高さ(=Selection.Height)を指定すれば良いことになります。(57~58行目)

2-2-2-9.貼り付けるサイズを決める(画像比維持)
次に「画像比維持」の場合ですが、「セル選択範囲の縦横比」と「画像の縦横比」の関係で処理が異なってきます。
図2-11の様に「画像より選択範囲が横長であれば高さを合わせ」、「画像より選択範囲が縦長であれば幅を合わせ」る事になります。


図2-12

まず61行目で、「セル選択範囲の縦横比」と「画像の縦横比」の比を計算し、選択範囲が縦長か横長かを選別しています。
Select Case文の評価式「 (Selection.Width / Selection.Height) / (Pict_W / Pict_H)」は、「横/縦」の比を比較しているものですが、図2-12で考えると、
(A)画像より横長=(3/1.5)/(4/3)= 約1.50
(B))画像より縦長=( 2/5 )/(4/3)= 約0.30
となり、「1以上であれば、選択範囲の方が横長(63行目以降)」「1以下であれば、選択範囲の方が縦長(67行目以降)」であることが分かります。
(選択範囲と画像の比がちょうど同じ(=1)の時は、セルフィットと同じ事になり、分類としては(A)でも(B)でも良いことになります。)

次に63行目の「選択範囲の方が横長」の場合ですが、図2-12の上側(A)のように、「Pict_H(Height)」には選択範囲の高さ(=Selection.Height)を指定すれば良いことが分かります。
また、「Pict_W(Width)」には、Selection.Heightに画像の縦横比を掛けた値を指定すれば良いことも分かります。

ただし、設定する順番があります。
先に「Pict_H(Height)」に「Selection.Height」を代入してしまうと、その後で「Pict_H(Height)」を使って計算(Selection.Height * (Pict_W / Pict_H))時に間違った結果となってしまいます。
ですから「Pict_W(Width)」への代入式(Selection.Height * (Pict_W / Pict_H))を先に計算することで計算間違いを防げます(図2-12の右側式の①②の順にする)。

67行目の「選択範囲の方が縦長」の場合は横長の時(63行目)と逆で、「Pict_W(Width)」には選択範囲の幅(=Selection.Width)を指定すれば良く、順番的にも「Pict_H(Height)」を先に計算しなければならない事も同じです。

なお今回、画像貼付け時の引数にPict_W・Pict_Hを用いていますが、別な「代入専用の変数」を設ければ、このような計算の順番は気にしなくてもOKになります。頭が混乱し易い場合は「代入専用の変数」をお勧めします。

2-2-2-10.画像の貼付け
73~77行目は、画像を貼り付ける部分です。1行を見易いように分割しています。
画像貼り付けメソッドとしては、今回の「ActiveSheet.Shapes.AddPicture」の他に「ActiveSheet.Pictures.Insert」というのがあります。2つの特徴を図2-13で比較してみます。
ActiveSheet. Shapes.AddPictureActiveSheet. Pictures.Insert
方式 既存のファイルから新規のShapeオブジェクトを作成 図をリンク・オブジェクトとして挿入
引数(必須)  FileName:=作成する図のファイル名
 LinkToFile:=リンクの有無
 SaveWithDocument:=図も一緒に保存するか否か
 Left:=図の左上隅の位置をポイント単位で指定
 Top:=図の左上隅の位置をポイント単位で指定
 Width:=図の幅をポイント単位で指定
 Height:=図の高さをポイント単位で指定
 貼り付ける図のファイル名

 (Left,Top,Width,Height等は併行で設定可)
Excelファイルサイズ 大(+画像ファイルサイズの分) 小(リンクのみの為)
図2-13

今回使用する「ActiveSheet.Shapes.AddPicture」メソッドでは、図2-13のように7つの引数を全て指定する必要があります。
1つ目の引数「FileName」には、ドロップしたファイル名ですので、16行目で取得したFilePathを指定します。
2つ目の引数「LinkToFile」は、デジカメ等とのリンクが切れる可能性がありますので、False(リンクせず)を選択します。
3つ目の引数「SaveWithDocument」は、図を一緒に保存するためTrueにします。
      これは、2つ目の引数「LinkToFile」でFalseを選んだ時点で「図を保存する」しか選択肢はありません。
4つ目の引数「Left」は「図の左上隅の位置」ですので、範囲選択した左端である「Selection.Left」を指定します。
5つ目の引数「Top」は「図の左上隅の位置」ですので、範囲選択した上端である「Selection.Top」を指定します。
6つ目の引数「Width」は、56~71行目で計算してきた結果である「Pict_W」を指定します。
7つ目の引数「Height」も同様に「Pict_H」となります。

この実行でシートに画像が貼り付けられます。


今回のシステムで、「ActiveSheet.Shapes.AddPicture」ではなく「ActiveSheet.Pictures.Insert」を使ってしまうと、元ファイルとのリンクが切れた場合に画像が表示できなくなってしまいます。
(もちろん「ActiveSheet.Shapes.AddPicture」でも、「LinkToFile:=True」「SaveWithDocument:=False」と設定すれば、Pictures.Insertと同じになり、リンク切れで画像表示が出来なくなります。)

例えば、デジカメのカードをPCに挿して「ActiveSheet.Pictures.Insert」メソッドの引数に写真のファイル先を指定して貼付けを行うと、カードを抜いたあとは画像が表示されなくなります。カードを抜いたことでリンクが切れてしまうからです。
ですので、リンクが切れる可能性がある場合は「ActiveSheet.Shapes.AddPicture」を使用するようにして下さい。
(逆に「社内でしか使用しないファイルにはリンク貼り付け」をすると、「持ち歩き用PCの中に機密性のファイルを保存していてもセキュリティが保てる」ことを意味するかもしれません。元画像は社内にあるのですから。)

尚、手動で図の挿入を行う際には、図2-14のように「挿入(S)」「ファイルにリンク(L)」「挿入とリンク(A)」が選べます。

図2-14

それぞれの動作は以下の通りです。
「挿入(S)」      :元画像のコピーが挿入される。
「ファイルにリンク(L)」:リンクを保ったまま挿入され、元画像が変更されると挿入した画像も変わる。
「挿入とリンク(A)」  :元画像がある場合はリンクが保たれ、リンクが切れても画像は残る。

貼り付けた画像を見てみると、「挿入(S)」は「ActiveSheet.Shapes.AddPicture」、「ファイルにリンク(L)」は「ActiveSheet.Pictures.Insert」を使用した結果と同等のもののようです。
しかし、この3種類を「マクロ自動記録」で記録してみると、全て「ActiveSheet.Pictures.Insert」となっています。
マクロ記録を全面信用すると痛い目に会う一例だと思います。良く調べてから使いましょう。


2-2-3.Pngファイルのサイズ取得

Excelの関数(図2-6の23~25行目)で画像サイズが取得できるのは上記の「JPG, GIF, BMP 等」であり、「PNG , TIFF」用の関数はありません。しかし今回のプログラムでは「画像貼付け時に貼付けサイズを指定する」必要があります(77行目)。
「PNG , TIFF は諦める」という手もありますが、それは悔しいですし実用性も低くなりますので、ファイル中に保存されている画像サイズをバイナリ単位で取り出して使用することにします。

2-2-3-1.デジタルで使用する単位の基本知識
まずは、デジタルで使う単位などの基礎知識から説明します。

デジタルとは「OFF」か「ON」で、見た目では「0」か「1」で表現されます。2に進もうとすると1桁上がらないといけないので「2進数」と呼ばれます。
人間界では「合計で1234円です」などの「1234」というような10進数を使っており、「0~9」までの数字が使えますので、4桁ならば0~9999までの値が表現できます。
一方、デジタル界(2進数)では4桁といっても「0~1」しか使えませんので、最大でも「1111」となります。

では、このデジタル界(2進数)での「1111」は、人間界(10進数)ではいくつになるのでしょうか。

図2-15

私たちの頭の中では、4桁の「1234(10進数)」を見た時、各桁の数字に「1」「10」「100」「1000」を掛けてから足し算をしています(図2-15の左側)。この各桁に掛ける値は、実は10進数の「10」に「桁番号から1を引いた数字」を階乗(例えば4桁目であれば、10(4-1) = 103 = 1000 )したものなのです。

では2進数ではどうなるかですが、10進数の時と同じように図2-15の右側の通り、各桁に「2進数の2」に「桁番号から1を引いた数字」を階乗したものを掛け、最後にそれらを足します。例えば「1111(2進数)」だと、
< 1111(2進数)  =  1x23 + 1x22 + 1x21 + 1x20  =  1 x 8 + 1 x 4 + 1 x 2 + 1 x 1 = 15
と、2進数の4桁は、10進数で言うところの「0~15」が表現可能 と分かります。

つぎに、この「0~15」の表現を図2-16の様に「0~9、A~F」で表す事にすると、10進数で言う16で桁が上がることから「16進数」と呼ぶことが出来ます。

図2-16

この16進数は、2進数4桁「1111」から出てきました。つまり「2進数4桁」は「16進数1桁」で表せることになるのです(図2-17の左側)。
そして、図2-17の右側のように、「2進数8桁」は「16進数2桁」で表すことができ、10進数で言うところの0~255が表現できます。

図2-17

2進数1桁は1ビット(「0」か「1」か)ですので、8桁は8ビットです。デジタル界では8ビットをまとめて「バイト」という単位で表します。「このハードディスクは500MB(メガ・バイト)だ」という時の「バイト」です。

この「1バイト」をテキストとしての文字や数字、及び記号に割り当てたのが「ASCIIコード」で、図2-18になります。

図2-18

図2-18の「16進」の行に「0x16」などと、数字の先頭に「0x」が付いているのに気が付いたかと思いますが、これは「16進数での値です」という意味です。単に「16」と書いてあっても「10進数の16なのか、16進数の16なのか」で示す値が違いますから、間違わないようにしているのです。
なお、Excel VBAの数式の中で16進数を表す場合には、先頭に「&H」をつけます。例えば「&HFF」は「16進数のFF」という事を表し、「10進数の 255 」という事になります。図2-17の右側が裏付けられた形ですね。

2-2-3-2.PNGファイルの構造
まずはPNGファイルの構造を見てみます(図2-19)。

図2-19

「PNGファイルである事の証明」は、先頭の「PNGヘッダ(8バイト)」及びIHDRチャンクの先頭の「データ長」・「Chunk Type」を見れば良いのです。ちょうど先頭から16バイト繋がっている部分です。
また、画像の幅と高さもその直後に入っていますので、続けて見ていけば取得できます。

2-2-3-3.PNGの画像サイズ取り出しプロシージャ
図2-6の35行目で、ファイル名を引数に呼び出される「Png_WH」プロシージャが図2-20です。
  1. '========== ⇩③ PNGファイルから画像幅と高さを返すプロシージャ =========
  2. Private Sub Png_WH(PngPath As String, Png_Width As Long, Png_Height As Long)
  3.  Dim FileNo As Integer       ’← ファイル番号
  4.  Dim ByData(0 To 23) As Byte   ’←バイナリデータ24バイト分を配列として宣言
  5.  FileNo = FreeFile            ’←空いているファイル番号取得
  6.  Open PngPath For Binary As #FileNo   ’←NGファイルをバイナリ形式で開く
  7.   Get #FileNo, , ByData         ’←先頭から24バイト取得
  8.  Close #FileNo             'ファイルをクローズ
  9.    '↓先頭から16バイトがPNGフォーマットに合っている場合
  10.  If (ByData(0) = 137) And (ByData(1) = 80) And (ByData(2) = 78) And (ByData(3) = 71) And _
  11.    (ByData(4) = 13) And (ByData(5) = 10) And (ByData(6) = 26) And (ByData(7) = 10) And _
  12.    (ByData(8) = 0) And (ByData(9) = 0) And (ByData(10) = 0) And (ByData(11) = 13) And _
  13.    (ByData(12) = 73) And (ByData(13) = 72) And (ByData(14) = 68) And (ByData(15) = 82) Then
  14.     '↓画像の幅を取得
  15.   Png_Width = ByData(16) * &H1000000 + ByData(17) * &H10000 + CLng(ByData(18)) * &H100 + ByData(19)
  16.     '↓画像の高さを取得
  17.   Png_Height = ByData(20) * &H1000000 + ByData(21) * &H10000 + CLng(ByData(22)) * &H100 + ByData(23)
  18.  End If
  19. End Sub
図2-20

PNGファイルには、先頭の16バイトに「PNGファイルとしての証明部分」、続いて4バイトに「画像幅」、4バイトに「画像高さ」の情報が入っています。
ですので、先頭から連続した24バイトを取り出し、「PNGの証明」+「幅の取得」+「高さの取得」をすれば良いことが分かります。

2-2-3-4.PNGファイルからデータを読み取る
では図2-20のコードを上の方から見ていきます。
83~84行目は、このプロシージャ内で使用する変数宣言です。

変数「FileNo」は、ファイル番号を代入するものです。
Excelブックを開くのとは異なり、画像ファイル・テキストファイル等を開く際は「Openステートメント」を使用します。開くときにはファイル名のほかに、O/S(Windows)が管理している「使用許可番号(=ファイル番号)」が必要となります。ファイルを開いた後は、そのファイル番号を使用して作業をします。

イメージとしては図2-21の「開いたブックを変数として保存」「以降はブック名の代わりに全て変数を使って作業をする」
にちょっとだけ似ているかもしれません。(ブックの変数≒ファイル番号)
  1. Sub Test()
  2.  Dim OpenExcel As Workbook
  3.  Dim Val As Variant
  4.  Set OpenExcel = Workbooks.Open("C:\sample.xlsx")     ’←ファイルを開き、戻り値を変数に入れる
  5.   Val=OpenExcel.worksheets("sheet1"). range("A1").value   ’←変数を使って値を取得
  6.  OpenExcel.Close                    ’←変数を使ってファイルを閉じる
  7. End Sub
図2-21

また変数「ByData」はバイト型の配列で、ファイル先頭から連続24バイトを取り出せば良いので24個の大きさで宣言します。
(バイナリ―ファイルからデータを取り出す場合、ファイル上のデータ位置はバイト単位で指定しますが、その位置はゼロから始まります。ですので、バイトを入れる配列のインデックスもゼロからにする方が、処理が楽になります。)

86行目の「FileNo = FreeFile」ですが、右辺のFreeFile関数は「空いているファイル番号」を返す関数です。何か他のプログラム(Excel以外も含めて)で既に使用されているファイル番号を使用するとエラーが発生しますので、それを避ける為です。

そのファイル番号を使用して、87行目「Open PngPath For Binary As #FileNo」で、画像ファイルをバイナリで開いています。
この中で「PngPath」は、このプロシージャへ引数として渡されたPNGファイルのファイル名です。
またファイル番号である「FileNo」の前に「#」がついていますが、今回の構文に限っては#が無くても動きます。とりあえずは慣例と受け取っておいて下さい。

88行目では、開いたファイルからデータを取り出します。コードは「Get #FileNo, , ByData」で、第二引数は空になっていますが、ここには読み込む位置を指定します。(空の場合は先頭からになります。詳しくは図2-27で説明します。)
また読み込む量としては、第三引数の大きさ分だけ読み込みますので、24バイト分を読みます。

これで、配列ByDataに先頭から24バイト分のデータが格納されましたので、89行目でファイルを閉じます。

2-2-3-5.PNGファイルの証明部分を確認する
92~95行目は1つのIF文で、格納した配列ByDataの16バイト分のデータを調べます。

図2-22

PNGファイルの先頭16バイトが証明部分ですので、図2-22に従って調べて行きます。但しバイトデータを数値としてExcelに取り込んだあとですから、配列の各要素には10進数でデータが入っていますので、注意が必要です。
配列の0~15番目が全て合っていたら「PNGファイルであることが証明された」ことになりますので、97行目・99行目を実行します。

2-2-3-6.画像幅と高さを取得する
97行目は「画像の幅」を取得する部分です。4バイトの中に書かれているので、配列ByDataのインデックス16~19の値を取り出せばよいのですが、各バイトをどのように合算すれば良いでしょうか。

図2-23の左側のように、例えば4バイト分に数値が収まっているとします。

図2-23

図2-17で説明した通り、4ビットで16進数1桁(0~15)が表現でき、その倍の8ビット(=1バイト)では0~255が表現できます。
極端な言い方をすれば「1バイトでは256進数」とも言えます。ですので、数値が4つのバイトに分かれて入っているので、
 1桁目のバイトの数値 x (256)0
 2桁目のバイトの数値 x (256)1
 3桁目のバイトの数値 x (256)2
 4桁目のバイトの数値 x (256)3
と、それぞれの桁を計算し、最後に合算すれば良いことになります。

しかし97行目の式を見てみると「256の何乗」ではなく、「&H100 の何乗」という風に書いてあります。これは、図2-23の右側の縦表に示すように「10進数の256 = 16進数の100(&H100)」ですので、
 1桁目のバイトの数値 x 2560 → &H1000 → 1
 2桁目のバイトの数値 x 2561 → &H1001 → &H100
 3桁目のバイトの数値 x 2562 → &H1002 → &H10000
 4桁目のバイトの数値 x 2563 → &H1003 → &H1000000
と同じことです。

数値は、配列ByDataのインデックス16~19に入っていますので、先頭バイトが4桁目、次が3桁目・・・となりますので、
 ByData(19)の数値 x 1      (10進数で 1 )
 ByData(18)の数値 x &H100   (10進数で 256 )
 ByData(17)の数値 x &H10000  (10進数で 65536 )
 ByData(16)の数値 x &H1000000 (10進数で 16777216 )
の合計値が「画像の幅」になります。ちなみに、この値の単位は「ピクセル」です。
もちろん&H(なんとか)を使わずに、カッコ内の10進数の式にしても問題ありません。但し、桁が大きくなると数字を書き間違う可能性がありますので、充分注意して下さい。

(2020/4/13追記)
なお97・99行目の式で、&H100 を乗ずるバイト値にCLng(・・・)とデータ型変換を行っていることについて追加説明します。
今回の式を簡単に書くと図2-23Bのようになります。
MsgBox の内容を計算する際に変数aと変数bとを比べた時、Integer型の方が有効範囲が広いので、計算結果の枠としてInteger型が用意されます。
  1. Sub Test3()
  2.  Dim a As Byte   ’←有効範囲 0~255
  3.  Dim b As Integer    ’←有効範囲 -32768~32767
  4.  a= 255
  5.  b= &H100    ’←10進数で256
  6.  MsgBox a * b     ’←結果を入れる枠のInteger型の有効範囲を超えるため「オーバーフロー」発生
  7.  MsgBox CLng(a) * b     ’←どちらかをLong型にすれば「オーバーフロー」が防止できる
  8. End Sub
図2-23B

&H100は10進数で256、1バイト内は0~255 ですから、掛け算をすると最大で65,280になります。Integer型は-32,768~+32,767ですのでオーバーする訳です。
計算結果の型は、どちらかサイズの大きい方の型が用意されます。ですのでエラーを防止するためには、9行目のようにどちらかの値を型変更し「Long型 xInteger型」とすることで、結果を入れる枠がLong型になりエラーを防止できる、という事になります。

ちなみに、図2-23Cの2行目でも「オーバーフロー」となります。数字の「256」はInteger型だからです。
  1. Sub Test4()
  2.  MsgBox 256 * 256     ’←数字の256はInteger型。掛け算でInteger型の有効範囲を超える
  3.  MsgBox 256& * 256     ’←数値の後に「&」をつける事でLong型に出来る
  4. End Sub
図2-23C

対策としては、4行目のように数値の後ろに「&」をつけるとLong型に変換できます。
尚、数値の後ろに付けらる他の型には、「@」はCurrency型(通貨型)、「!」はSingle型、「#」はDouble型 があります。

この項をアップした後にエラーが出る事に気づきました。もしご迷惑をおかけしたとしたら申し訳ありませんでした。


99行目は「画像の高さ」の取得ですが、「画像の幅」に対し、配列ByDataのインデックスが20~23に変わるだけです。

なおIF文の92~95行目で、条件式がTrueではない(=PNGではない)場合は、Png_Width、Png_Heightの計算がされませんので、返す値はゼロになります。
つまり図2-6の36行目の「If Pict_W = 0 Or Pict_H = 0 Then」は「PNGファイルでは無い」事を意味しています。

ちなみに、図2-23の下側に書きましたが、4バイト(=32ビット)で表現できる値は「0 ~ 4,294,967,295」 であり、Long型(4バイト)の数値の範囲「-2,147,483,648~ 2,147,483,647」と幅が同じです。

2-2-4.Tiffファイルのサイズ取得

つぎにTiffファイルをバイト単位で解析し、画像の幅・高さを取得していきます。

2-2-4-1.バイトオーダーの基礎知識
ファイルを解析するために必要となる、複数バイトに1つの値を記録する際の並び順について説明します。
並び順を「バイトオーダー」また「エンディアン」と言いますが、図2-24のように、
ビッグエンディアン:先頭(上位のバイト)の方から配置していく方法
リトルエンディアン:後ろ(下位のバイト)の方から配置していく方法

と言います。

図2-24

ファイル形式によって、以下の通りどちらを使っているかが分かれるようです(調べられる限り)。
 ・ビッグエンディアン(モトローラ型:MM):PNG、JPG
 ・リトルエンディアン(インテル型:II):BMP、GIF、ICO
 ・両方:TIFF、RAW

前項のPNGの際は「先頭の方のバイトが上の桁」として読み込んでいましたが、これはPNGファイルが「ビッグエンディアン式」であるからです。

2-2-4-2.Tiffファイルの構造
では、TIFFのファイル構造を見てみます。(図2-25)


図2-25

ファイル中にファイル情報の領域IFD(Image File Directory)があり、そのIFDの場所は「IFDポインタ」に記してあります。
IFDは複数のエントリーで構成されており、その中の1つのエントリーに画像幅・高さ(それぞれ別のエントリー)の情報が入っています。また、IFDが何個あるかは、IFD先頭の「エントリーカウント数」に入っています。

1つのエントリーは12バイトの長さで、以下の4つに分かれています。
 ・ID   :データの種類を表します。図2-25の右表のように決められています。
 ・Type :データの型(1=BYTE型(1byte)、2=ASCII型(1byte)、3=SHORT型(2byte)、4=LONG型(4byte))
 ・Count:データの個数
 ・DATA :データが4バイトで収まる場合はここに書かれる。入りきらない場合はデータ場所を示すポインタになる。

2-2-4-3.Tiffの画像サイズ取り出しプロシージャ
Tiffの画像サイズ取り出すプロシージャが図2-26になります。
  1. '========== ⇩④ Tiffファイルの画像幅と高さを返すプロシージャ ============
  2. Private Sub TIF_WH(TiffPath As String, Tif_Width As Long, Tif_Height As Long)
  3.  Dim i As Long                    ’←IDFポインタの変数
  4.  Dim j As Long                    ’←エントリーカウント数
  5.  Dim FileNo As Long                  ’← ファイル番号
  6.  Dim Fin As Long                    ’← 幅・高さを調べ終わったかの確認の為の変数
  7.  Dim ByData() As Byte                 ’← バイトデータを入れる配列
  8.  Dim MMII As Long                   ’← バイトオーダーの値を入れる変数
  9.  FileNo = FreeFile                  ’← 空いているファイル番号を取得
  10.  Open TiffPath For Binary As #FileNo          ’← 画像ファイルを開く
  11.   ReDim ByData(1)
  12.   Get FileNo, , ByData
  13.    MMII = ByData(1)                  ’←77:モトローラ型 73:インテル型
  14.   ReDim ByData(3)
  15.   Get FileNo, 5, ByData                ’←IDFポインタからバイナリデータ取得
  16.    i = Tif_Num(ByData, MMII)             ’←IDFポインタを取得
  17.   ReDim ByData(1)
  18.   Get FileNo, i + 1, ByData              ’←エントリーカウンタからバイナリデータ取得
  19.    j = Tif_Num(ByData, MMII)            ’←エントリーカウント数を取得
  20.    i = i + 2                      ’←1つ目のエントリーの位置に設定
  21.    Do Until j = 0
  22.     ReDim ByData(1)
  23.     Get FileNo, i + 1, ByData            ’←エントリーのIDを取得
  24.      Select Case Tif_Num(ByData, MMII)       ’←エントリーIDを調べる
  25.       Case &H100                 ’←幅(0100)だったら
  26.        Tif_Width = Get_Value(FileNo, i, MMII)   ’←その後ろのデータを取得
  27.        Fin = Fin + 1               ’←幅を取得したら+1する
  28.       Case &H101                ’←高さ(0101)だったら
  29.        Tif_Height = Get_Value(FileNo, i, MMII)  ’←その後ろのデータを取得
  30.        Fin = Fin + 2              ’←高さを取得したら+2する
  31.      End Select
  32.      If Fin = 3 Then Exit Do            ’←幅と高さを取得したら終了
  33.       i = i + 12
  34.       j = j - 1
  35.    Loop
  36.  Close FileNo
  37. End Sub
  38. '========== ⇩⑤ 各エントリーの中からデータを取り出す関数 ========
  39. Private Function Get_Value(FileNo As Long, i As Long, MMII As Long) As Long
  40.  Dim ByData() As Byte
  41.  ReDim ByData(1)
  42.  Get FileNo, i + 3, ByData                ’←データ型を取得(3=short型,4=long型)
  43.   If Tif_Num(ByData, MMII) = 4 Then ReDim ByData(3)   ’←Long型だったら配列増
  44.  Get FileNo, i + 9, ByData                 ’←画像幅・画像高さデータを取得
  45.   Get_Value = Tif_Num(ByData, MMII)            ’←画像幅・画像幅を計算し返り値にする
  46. End Function
  47. '========== ⇩⑥ バイトから値を取り出す関数 ===============
  48. Private Function Tif_Num(ByData() As Byte, MMII As Long) As Long
  49.  Dim Num As Long
  50.  If MMII = 73 Then                    ’←インテル型の場合(リトル・エンディアン)
  51.   Num = ByData(0) + CLng(ByData(1)) * &H100         ’←Short型での計算
  52.   If UBound(ByData) > 1 Then              ’←Long型か否かを調査
  53.    Num = Num + ByData(2) * &H10000 + ByData(3) * &H1000000
  54.   End If                         '↑Long型の場合に計算追加
  55.  ElseIf MMII = 77 Then                 ’←モトローラ型の場合(ビック・エンディアン)
  56.   If UBound(ByData) = 1 Then             ’←Short型での計算
  57.    Num = ByData(1) + CLng(ByData(0)) * &H100
  58.   Else                          ’←Long型での計算
  59.    Num = ByData(3) + CLng(ByData(2)) * &H100 + _
  60.       ByData(1) * &H10000 + ByData(0) * &H1000000
  61.   End If
  62.  End If
  63.  Tif_Num = Num                      ’←画像幅・画像幅を返す
  64. End Function
図2-26

104~109行目はプロシージャ内で使用する変数の宣言です。
この中で108行目の「ByData」配列は、動的配列として宣言しています。PNGの時には24バイトで固定でしたが、Tiffの場合は様々な大きさのデータを取得・判断する必要があるからです。
また107行目の「Fin」変数は、画像の幅と高さのデータを取得完了したら今回は任務終了ですから、他のエントリーを調べるのを止めるために準備した変数です。
109行目の「MMII」変数は、前項(2-2-4-1項)のバイトオーダーで説明した「ビッグエンディアン」「リトルエンディアン」かを示す値を代入し、バイト単位の計算方法を切り替えるために使用します。

まずPNGの時と同様に、111行目で空いているファイル番号を取得し、112行目でファイルを開きます。
次に114~116行目で、先頭2バイトに記録してある「バイトオーダー」を読み取ります。
1バイト目、2バイト目には同じ値が入っており、その値は以下のことを示しています。
 ・モトローラ型(ビッグエンディアン)=0x4D(10進数で77)
 ・インテル型(リトルエンディアン)=0x49(10進数で73)

114行目で「ByData」動的配列の大きさを「0~1」と、2つのバイトが入る大きさに変えます。
115行目では、GETの第二引数を指定していないことから、ファイルの先頭から2バイトをByData配列に取り込みます。
116行目では、ByData配列の2要素目のデータを変数MMIIに代入しています。

次に118~120行目で「IDFポインタ」を読み取ります。
118行目で「ByData」動的配列の大きさを「0~3」と、4バイトが入る大きさに変えます。
119行目でGetの第二引数に5を設定することで「5番目のバイトから4バイト分のデータ」を取り込みます。
取得した「IDFポインタのバイトデータ」と「バイトオーダー種類」を、120行目の関数「Tif_Num」に渡すことで「IDFポインタ」が得られ、変数iに代入します。

122~124行目は「エントリーカウント数」を読み取ります。
エントリーカウント数は2バイトに収められていますので、122行目で「ByData」動的配列の大きさ0~1の2バイトサイズに変えています。
123行目では120行目で得た「IDFポインタ」の位置のデータを読み取ります。
123行目で読み取る位置を「i+1」としていますが、ポインタの位置とGetの位置には少し違いがあります(図2-27)。


図2-27

ポインタは「読み取る位置の先頭」を指すものですが、Getは「読み取りを開始する場所のバイト番号」です。
図2-27の通り、例えば120行目で得るi(ポインタ)が「8」であった場合、読み取り開始バイトは「9」になります。
よって、123行目のGetの第二引数に指定する値は「i+1」を指定します。
124行目は、取得した「エントリーカウント数のバイトデータ」と「バイトオーダー種類」を「Tif_Num」関数に渡すことで「エントリーカウント数」が得られ、変数jに代入します。

まず変数iは、120行目で得たままですので「IDFポインタ」です。IDFの先頭に2バイトのエントリーカウントがあり、その後ろから各エントリーが始まっています。
ですので、126行目で「i = i + 2」とすることで、エントリーカウントに続くエントリー本体を示すポインタとなります。

127~142行目では、エントリー数分だけエントリーの中を調べています。エントリー数は124行目で得たj値ですが、141行目の「j = j - 1」でj値を変えながらDo~Loopを回しています。

128行目では、「ByData」動的配列の大きさを「0~1」と2バイトが入る大きさに変え、129行目で各エントリーの先頭の2バイトを読み取ります。
エントリー先頭には「エントリーのID(種類)」が収められています。131行目では、Tif_Num関数でID値に変え、その値でCase選別しています。

「エントリーのID(種類)」は図2-25の右表の通りで、今回の目的である「画像幅」・「画像高さ」は、それぞれ「0x0100」・「0x0101」です。
132行目では「画像幅(0x0100 = Excelでは&H100)」、135行目では「画像高さ(0x0101 = Excelでは&H101)」である場合の処理をしています。
1つの「エントリーのID(種類)」を調べ終わると、次のエントリーに移ります。1つのエントリーのサイズは12バイトですので、140行目で「i = i + 12」とポインタを移動させています。

「エントリーのID(種類)」が「画像幅(0x0100 = Excelでは&H100)」の場合には133~134行目を実施します。
133行目では、Get_Value関数に「ファイル番号」「エントリーのポインタ位置」「バイトオーダー値」を渡して、そのエントリーの「データ」を取得しています。取得したデータは画像幅のピクセル値となります。
(Get_Value関数の詳細については、後述します)

「エントリーのID(種類)」が「画像高さ(0x0101 = Excelでは&H101)」の場合には136~137行目を実施します。
136行目では、133行目と同様で画像高さのピクセル値を取得します。

変数Finに値を代入している134行目・137行目、及び139行目ですが、これは「画像の幅・高さの情報を吸い上げ終わったかのチェック」になります。
IDFの中のどのエントリーに画像幅の情報、画像高さの情報が入っているかはわかりませんので、Do~Loopで調べていき、「画像幅を取得」したら変数Finに1を加え、「画像高さを取得」したら変数Finに2を加えていきます。
そして、1つ1つのエントリーを調べる最後で変数Finが3になっていれば「画像幅と鷹さの両方を取得した」と判断でき、「あとは調べても無駄」なのでDo~Loopを抜け出します。

今回は「画像幅を取得したら+1」「画像高さを取得したら+2」としていますが、「画像幅のエントリーが複数有るわけではない」ので、「幅・高さとも+1」「Finが2になったら終わり」でも問題は有りません。
また、もっと直接的に「Tif_Width」x「Tif_Height」の値がゼロでなくなったら終わり、でもOKです。
要は「幅・高さの両方を取得完了したら終了」が成立する式を作れば良いわけです。

143行目では、112行目で開いた画像ファイルを閉じ、メインプロシージャ側に画像幅(Tif_Width)・画像高さ(Tif_Height)を引数で渡し、プロシージャを終了します。

2-2-4-4.Tiffファイルからデータを読み取る
146~155行目が「Get_Value関数」で、133行目・136行目から呼び出されています。
引数は3つで、内容は以下の通りです。
 FileNo:112行目でオープンしたファイル番号。
 i   :各エントリーの先頭のポインタ。
 MMII :バイトオーダーの種類。

147行目はこのFunctionの中で使用する動的配列の宣言です。TIF_WHプロシージャ(103~144行目)でも同じByDataという名前で動的配列が使用されていますが、無関係です。
また、MMIIについては、この関数内では直接計算に使用することは無いのですが、152行目・154行目で別関数Tif_Numを呼び出す時に必要なため、引数の一つにして取得しています。

149行目で、その動的配列の大きさを0~1の2バイトに変更しています。
150行目で、エントリーの3~4バイト目にある「Type」(図2-25参照)を読み取ります。
この「Type」には「このエントリーのデータ型」が1~12(10進数)で示されています。
 1:Byte型(1バイト)
 2:ASCII型(1バイト)
 3;Short型(2バイト)
 4:Long型(4バイト)
 :  :
今回の画像幅・高さは、3か4(Short型かLong型)です。

153~154行目で画像の幅・高さを配列ByDataに取り込みますが、Short型で入っている時は149行目で変更した2バイトのままの大きさで良いのですが、Long型で入っている時にはサイズを4バイトにしなければなりません。
ですので152行目では、その読み取った「Type」が「4(=Long型)」だったには、配列ByDataの大きさを0~3の4バイトに変更しています。

153行目では各エントリーの9番目のバイト以降を読み取り、154行目で10進数に変換したのち関数の戻り値にします。

なお、今回のプログラムでは画像幅・高さのデータしか取得しませんので、データは1回取得すれば完了です。
しかし他のエントリーの中には、複数のデータが入っているエントリーも存在します(文字列や画像データのポインタなど)。
その時には1つ手前のCount(4バイト)部分にデータの個数が記録されていますので、そちらも読み込んでから繰り返しデータを取得する必要が出てきます。

2-2-4-5.バイトデータを10進数に変換する
157~173行目が「Tif_Num関数」で、各所から呼び出されています。
引数は2つで、内容は以下の通りです。
 ByData:バイトデータの入った配列(配列要素数は可変。今回は2要素または4要素)
 MMII :バイトオーダーの種類(77=ビッグエンディアン、73=リトルエンディアン)

プログラムは、まず「ビッグエンディアン」なのか「リトルエンディアン」なのかで分け、またその中でByData配列が「2要素(Short型)」なのか「4要素(Long型)」なのかで分けて処理をしています。

160~163行目が「リトルエンディアン」の場合です。
図2-28の通り、リトルエンディアンの場合はByData配列が2要素(Short型)でも4要素(Long型)でも、1要素目が最下位の1桁目、2要素目が2桁目になります。


図2-28

ですので、160行目では1桁目・2桁目の計算をまず実施しています。計算方法はPNGファイル時の計算と一緒で、1桁目にはx1を2桁目にはx&H100(10進数でx256)を掛けて、それぞれを足します。
次に161行目でByData配列の最終インデックスをUbound関数で求めます。Short型なら1、Long型なら3が出ますので、Long型(>1)なら162行目を実行します。
162行目では3桁目・4桁目を計算し、160行目で計算した1桁目・2桁目の合計値に足します。
計算方法は、3桁目(ByData(2) )x&H10000、4桁目(ByData(3) )x&H1000000 です。

160~163行目が「ビッグエンディアン」の場合です。
ビッグエンディアンの場合は図2-29の通り、Short型(2バイト)・Long型(4バイト)で各バイトに掛ける値が全く異なります。

図2-29

ですので、165行目のIF文ではUbound関数で最終インデックスを調べ、Short型(最終インデックス=1)だったら166行目を、Long型(最終インデックス=3)だったら168~169行目を実行させています。
計算の方法は、Short型では1桁目に&H100を掛け、2桁目と足し合わせます。
Long型では1桁目x&H1000000、2桁目x&H10000、3桁目x&H100、4桁目x1 をそれぞれ掛け、足し合わせます。

最後に172行目で、各バイト間の計算結果である変数Numを関数の戻り値にします。

2-3.標準モジュールのコード

標準モジュールに記載するコードは、図2-30の通りです。
176行目は、モードレス(フォームを表示させたままシート側で作業可)でフォームを表示します。
  1. '========== ⇩⑦ フォームを起動するプロシージャ ===============
  2. Public Sub Picture_Paste()
  3.  UserForm1.Show 0           ’←モードレスでフォームを表示
  4. End Sub
図2-30

3.ListView の代わりに WebBrowser を使う方法について

最初の方で「ListViewが使えない場合はWebBrowserを」と書きました。WebBrowserコントロールにはドラッグ&ドロップのイベントがあり、ドロップしたファイル名が取得できます。
もともとは、読んで字の如くIEやEdgeのようにサイトをExcelフォーム上に表示させるものですが、持てる機能を活用してみる価値はあると思います。

第2項のListViewに対する違いは、下記三点です。
1)フォームに配置するコントロールが ListView → WebBrowser に変わる
2)コントロールのプロパティが異なる(フォームのInitializeイベントでの設定を変更)
3)画像ファイルをドロップした時に発生するイベントが異なり、受け取る引数も異なる

3-1.WebBrowserコントロールの配置

WebBrowserコントロールも標準では表示されていませんので、まず図3-1の方法でツールボックスに追加をします。

1)ツールボックス上でマウスを右クリックし、「その他のコントロール」を選択します。
2)リストの中から「Microsoft Web Browser」を選択(先頭にレ点を付ける)しOKボタンを押します。
3)ツールボックスにWebBrowserマーク「」が追加されます。


図3-1

ツールボックスにWebBrowserが追加できましたら、クリックしてフォーム内に図3-2の様にWebBrowserコントロールを配置します。


図3-2

WebBrowserの範囲は真っ黒となりますが、実行時は白背景になります。
他にはOptionButtonを2個配置し「セルフィット」と「画像比維持」とすることは、ListView時と同じです。

3-2.WebBrowser用フォームのInitializeイベントプロシージャ

フォームを起動する時にフォームの初期化をするのが図3-3のInitializeイベントプロシージャです。
  1. '========== ⇩⑧ フォームの初期設定(WebBrowser用) ============
  2. Private Sub UserForm_Initialize()
  3.  'Me.ListView1.OLEDropMode = ccOLEDropManual  ’←ListViewのプロパティ設定(不要)
  4.  Me.Caption = "画像トンネル"
  5.  Me.OptionButton2.Value = True         ’←画像比維持を標準にする
  6. End Sub
図3-3

変わっているのは、180行目(ListViewのプロパティ設定)を削除しているところです。WebBrowserは作ったままの状態で問題ありません。
(見栄え的に「WebBrowserに外枠線の設定」をしたかったのですが、プロパティが見当たらず、そのままで使用しています。)

3-3.WebBrowserのイベントプロシージャ

画像ファイルをドロップした時に発生するイベントは、WebBrowserでは「BeforeNavigate2」イベントになります。
その引数の内の「URL As Variant」で、ドロップしたファイルの「パス名+ファイル名」が受け取れます。

尚、PNGファイル、Tiffファイルの画像サイズ計算部(図2-20、図2-26)は全く同じものを使用します。
  1. '========== ⇩⑨ ファイルをドロップした時のイベントプロシージャ(WebBrowser) ===========
  2. Private Sub WebBrowser1_BeforeNavigate2(ByVal pDisp As Object, URL As Variant, Flags As Variant, TargetFrameName As Variant, PostData As Variant, Headers As Variant, Cancel As Boolean)
  3.  Dim FilePath As String   ’←ドロップしたファイルのPATH付ファイル名
  4.  Dim Pict_W As Long     ’←画像ファイルの幅
  5.  Dim Pict_H As Long     ’←画像ファイルの高さ
  6.  Dim Err_No As Long     ’←エラー番号を一時保存
  7.  'Dim cnt As Long       ’←カウンタ変数(=一度にドロップしたファイル数)
  8.  Cancel = True     ’←(追加)キャンセルしないと画像がコントロール内に表示される
  9.  'For cnt = 1 To Data.Files.Count   ’←複数ファイルをドロップした際は、1つずつ処理
  10.   'FilePath = Data.Files(cnt)    ’←ドロップしたファイルのファイル名を取得(ListView)
  11.   FilePath = URL           ’←ドロップしたファイルのファイル名を取得(WebBrowser)
  12.            '↓ファイル名の最後のピリオド以降を調べる
  13.   Select Case UCase(Split(FilePath, ".")(UBound(Split(FilePath, "."), 1)))
  14.            '↓JPG,GIF,BMPなどの画像ファイルの場合
  15.    Case "JPG", "JPEG", "GIF", "BMP", "ICO", "RLE", "WMF", "EMF"
  16.     Dim PP As Object
  17.     On Error Resume Next
  18.      Set PP = LoadPicture(FilePath)    ’←ファイルを画像としてPP変数に取り込み
  19.      Pict_W = CLng(PP.Width * 0.0378)   ’←画像の幅を計算
  20.      Pict_H = CLng(PP.Height * 0.0378)   ’←画像の高さを計算
  21.      Err_No = Err
  22.     On Error GoTo 0      ’←エラー発生時はForを脱出しエラー処理完了出来ない為、事前に完了させる
  23.     If Not Err_No = 0 Or Pict_W = 0 Or Pict_H = 0 Then   ’←エラーが出たかサイズがゼロかの場合
  24.      MsgBox "画像ファイルでは無いか" _
  25.          & vbCrLf & "サイズ認識不可です"      ’←エラーコメントを出し
  26.      'goto NextPicture                ’←Forを抜け、次のファイル処理へ行く
  27.      Exit Sub                    ’←マクロ終了
  28.     End If
  29.    Case "PNG"                    ’←PNGの場合
  30.     Call Png_WH(FilePath, Pict_W, Pict_H)    ’←ファイルの縦横を取得
  31.     If Pict_W = 0 Or Pict_H = 0 Then       ’←エラーが出ていれば
  32.      MsgBox "PNGファイルでは無いか" _
  33.          & vbCrLf & "サイズ認識不可です"   ’←コメントを出して
  34.      'goto NextPicture              ’←処理を終了する
  35.      Exit Sub                  ’←マクロ終了
  36.     End If
  37.    Case "TIF", "TIFF"               ’←Tiffの場合
  38.     Call TIF_WH(FilePath, Pict_W, Pict_H)   ’←ファイルの縦横を取得
  39.     If Pict_W = 0 Or Pict_H = 0 Then      ’←エラーが出ていれば
  40.      MsgBox "TIFFファイルでは無いか" _
  41.          & vbCrLf & "サイズ認識不可です"   ’←コメントを出して
  42.      'goto NextPicture              ’←処理を終了する
  43.      Exit Sub                  ’←マクロ終了
  44.     End If
  45.    Case Else
  46.     MsgBox "画像ファイルでは無いか" _
  47.         & vbCrLf & "未対応のファイルです"   ’←拡張子が画像では無いファイルの場合
  48.     'goto NextPicture
  49.     Exit Sub                    ’←マクロ終了
  50.   End Select
  51.   If Me.OptionButton1.Value = True Then   ’←セルフィットの場合
  52.    Pict_W = Selection.Width         ’←選択範囲幅を画像幅にする
  53.    Pict_H = Selection.Height        ’←選択範囲高さを画像高さにする
  54.   Else                     ’←画像比維持の場合
  55.    Select Case (Selection.Width / Selection.Height) / (Pict_W / Pict_H)  ’←貼付範囲と画像の縦横比を比較
  56.     Case Is >= 1              ’←選択範囲の方が横長の時
  57.      Pict_W = Selection.Height * (Pict_W / Pict_H)
  58.      Pict_H = Selection.Height
  59.     Case Is < 1               ’←選択範囲の方が縦長の時
  60.      Pict_H = Selection.Width * (Pict_H / Pict_W)
  61.      Pict_W = Selection.Width
  62.    End Select
  63.   End If
  64.   ActiveSheet.Shapes.AddPicture _
  65.     FileName:=FilePath, _
  66.     LinkToFile:=False, SaveWithDocument:=True, _
  67.     Left:=Selection.Left, Top:=Selection.Top, _
  68.     Width:=Pict_W, Height:=Pict_H       ’←画像貼り付け
  69. 'NextPicture:   
  70.  'Next cnt   
  71. End Sub
図3-4

ListViewに対して変更した部分は、追加は赤字・削除は見え消し線にしてあります。

まず185行目のイベント自身は当然変更になります。
192行目は「Cancel = True」としているのは、WebBrowserはサイト等を表示するために存在しますので「ドロップしてきたファイルを表示」しようとします。しかしコントロール内に表示されても困るので、イベントでの変更をCancelしているのです。

193行目のFor~Nextですが(Nextは256行目)、ListViewでは複数ファイルを同時に受け取ることが可能でしたが、WebBrowserが受け取れるのは1つのみです。
For~Nextをそのまま残しておいても支障はありませんが、機能しませんので削除します。
また、複数ファイルを数えているcnt変数も不要となるため、190行目も削除します。

1つしか受け取れないのは、本来のWebBrowserの用途は「ExcelでWebサイトを表示させる」事にあり、1画面で複数のURLを同時に表示することはないという前提で作られているためと思われます。(推定)
194~195行目は、ファイル名を取得する部分です。ListViewの時は「Data.Files(cnt)」という引数で取得していましたが、WebBrowserでは引数「URL」から取得します。

210~211、218~219、226~227、232~233行目の修正は、ListViewの複数ファイル対応の時は「次のファイルの処理に移動するため、エラーが出たらNextPictureへ飛ぶ」ことにしていましたが、単一ファイルしか受け付けないWebBrowserではその必要が無いため「Exit Sub」でマクロ終了させています。
また、エラー時の飛び先である255行目「NextPicture:」も不必要となります。

なお、ダイアログ上に「複数ファイルをドロップすることは出来る」のですが、WebBrowserイベントでは「何個のファイルをドロップしたのか受け取れない」ために、コメントを出すことも出来ません。WebBrowser側を使用する場合は、使い方を周知させる必要があります。

3-4.WebBrowser用の標準モジュール

最後に、フォーム起動用のプロシージャを、WebBrowser側が立ち上がるように修正します。
  1. Public Sub Picture_Paste()
  2.  'UserForm1.Show 0     ’←モードレスでUserForm1を表示する(ListView)
  3.  UserForm2.Show 0     ’←モードレスでUserForm1を表示する(WebBrowser)
  4. End Sub
図3-5

図3-5を実行すると、図3-6の様にWebBrowserコントロールで作ったフォームが立ち上がります。

図3-6

4.Excelのアドインファイルにする

アドインへの登録方法については、下記を参照願います。ファイル名が異なるだけで手順は全く同じです。
・「セルの罫線を矢印キーで引く
・「西暦・和暦対照表


5.最後に

今回のマクロは、報告書などのExcel台紙にデジカメで撮った写真を貼付けるような場面で活用できるような気がします。
尚、ListViewが64bit版Excelで動かない話がありますので、社内に展開などする場合は充分注意をお願いします。

サイズの大きな画像ファイルをドロップすると、意外と時間がかかります。これはLoadPictureでExcelに画像ファイルを1回取り込んでいるためのようです。
LoadPictureを使わずに「とにかく貼り付けてからサイズを直す」という方法を紹介しているサイトもありますので、近い内に試作し比較してみたいと考えています。

また、今回PNG・TIFFのファイルフォーマットを調べてみて、ファイル仕様の多様性を改めて感じました。要件も多様なので仕方がないとは思いますが、それを吸収するような関数(今回で言う LoadPicture 関数)を数多く開発してくれることを期待する反面、バイト単位で必要なデータを取り出す面白さみたいなものを感じたマクロでした。


画像を直接シートに貼り付ける(it-023A.xlsm)

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