先日、フォロワーのA氏がWindowsMediaPlayerを使ったVBAで、環境依存のバグに悩んでいたので解決した話をする。
WindowsMediaPlayerとは
WindowsMediaPlayer(略称WMP)は一般的なWindows PCなら必ずと言って良いほどインストールされている動画再生ソフトである。
厳密には普通のWindows 7,8,10をクリーンインストールした時点で嫌でも導入される。企業の情シスが「機能の追加と削除」から削除している場合や、EU向けのN,NK版OS、RT版のOSを使っている場合などは入っていない可能性があるが、まあ普通は無視しても良いだろう。
もし入っていない不幸な人は Windows Media Player の入手
最新版は12でWindows 7以降バージョンが変わっていない。ただし、OS標準コーデックが増えているので、10で再生できる動画が7では再生できない・音声が出ないと言った事が起こるかもしれない。
詳しくは Wikipediaに任せる
VBAでWMPを使う
VBAで外部コンポーネントを使うには、アーリーバインディング(事前バインディング)とレイトバインディング(実行時バインディング)の2種類の選択肢がある。
2つの違いを簡単に言うと、予め型をはっきりさせてコンパイルチェック等もしっかりした上で実行するか、Object型変数で型が不明なまま実行して、実際にインスタンスを作成する場面にたどり着いて初めて型が確定する。という感じである。
下手な説明で申し訳ないが、これらの語句で検索すれば誰かが上手に説明してくれているはず。
それと、CLSID(クラスID)やProgIDという言葉が出てくるが、とても詳しく解説してくれている記事を教えてもらったので追記しておく。
WSHのCreateObject関数の引数のCOM識別子「ProgID」「CLSID」(GUID)とは何なのか解説。Windows内のActiveXオブジェクトを一覧表示して確認するコマンド
アーリーバインディング(事前バインディング)
一番オーソドックスなのは、参照設定して使うことだろう。
ところが、参照設定を開くとWindows Media Player
というライブラリが2つ出てくるかもしれない。
それぞれのパスを確認すると以下のようになっている。
C:\WINDOWS\system32\wmp.dll C:\Windows\System32\msdxm.tlb
wmp.dll
が本物のような気がするが、これだけでは確証がない。
実はツールボックスからWMPを設置することで確認できる。(というかこっちのやり方が普通か?)
つまりC:\WINDOWS\system32\wmp.dll
が本物である。
そもそも、コントロールを配置すると、勝手に参照設定が行われるので開発者自身はパスを覚える必要は無い。
あるとすれば、ブックを他のPCに持っていった時に、参照設定が切れてしまった場合にやり直す場面くらいだろうか。
使い方は普通のラベルやボタンと同じように、ツールボックスからGUI上で配置するだけなので説明する必要はないと思う。
もちろん、プロパティウィンドウで初期値を変更することができる。
コードから動的に生成しようかと思ったのだが、エラーが出てしまった。
Dim wmp As WindowsMediaPlayer Set wmp = New WindowsMediaPlayer
実行時エラー '-2147467259 (80004005)': オートメーション エラーです。 エラーを特定できません
既存のVBやVBSで動いている風なサンプルコードが見受けられるのが気になるが、解決策は見つからなかったので諦める。
後ほど解説するが、以下の様な感じで設置することになりそうだ。
'ちょっと使いたいだけ Dim wmp As WindowsMediaPlayer Set wmp = Me.Controls.Add("WMPlayer.OCX.7") '常駐させてイベントもフックしたい Public WithEvents wmp As WindowsMediaPlayer Set wmp = Me.Controls.Add("WMPlayer.OCX.7")
また、アーリーバインディングで宣言した変数はインテリセンスが働くので便利に使える。
メリットをまとめると
などである。
一方でデメリットは
- 参照設定が環境によって切れる
- Excelブックを開くたびに警告が出る
- ふとした拍子に参照設定が壊れる。運が悪いとブックが開けなくなりオダブツも
等と、少々怖い問題が挙げられる。
参照設定が環境によって切れる
Excelブックを開くたびに警告が出る
Microsoft Forms
このアプリケーションは、安全でない可能性のある ActiveX コントロールを初期化しようとしています。このファイルの提供元が信頼できる場合は、[OK] をクリックします。コントロールは現在のワークスペースの設定で初期化されます。
OK キャンセル
ふとした拍子に参照設定が壊れる
上記警告をOKしても以下のようなエラーが出る。
Microsoft Visual Basic for Applications
システム エラーです: &H80004005 (-2147467259) エラーを特定できません
OK ヘルプ
Microsoft Visual Basic for Applications
メモリが不足しています。
OK ヘルプ
フォームをデザインモードで開こうとしても出る。
もしこんな状態になってしまった場合、フォームのデザイン情報を復活させる事はできるのだろうか?
レイトバインディング(実行時バインディング)
もう一つの方法が、Object型変数にCreateObject
で生成したインスタンスを記憶させる方法。
'いずれも動かない Dim wmp As Object Set wmp = CreateObject("WMPlayer.OCX") Set wmp = CreateObject("WMPlayer.OCX.7")
最後の.7
は付けたり消したり。情報が錯綜しており、どちらにすべきかよく分からない。
最初は動いていた気がするのだが、今では何度試しても以下のエラーが出るため動かない。
実行時エラー '-2147467259 (80004005)': オートメーション エラーです。 エラーを特定できません
調べていたらCLSIDを使う例を発見し、手元の各種環境でも問題なく動きそうな感じだった。
Qiita - VBAでMediaPlayerを搭載させてみた
'WindowsMediaPlayerのインスタンスを生成 Set wmp = CreateObject("new:{6BF52A52-394A-11d3-B153-00C04F79FAA6}")
余談だが、これは偶にクリップボードでお目にかかる奴だ。
'DataObjectのインスタンスを生成 Set clipboard = CreateObject("new:{1C3B4210-F441-11CE-B9EA-00AA006B1A69}")
ただしCreateObjectしただけでは、フォームコントロールには配置されない。
C#等ではインスタンスを作成した後にフォームやコンテナにAddするのが一般的である。
今まで知らなかったのだが、VBAではインスタンスをフォームに設置することはできないようだった。
'こういう事はできない。 Me.Controls.Add wmp
音楽を再生したいような場面ではこの方法で良いが、動画を再生したいならコントロールとして配置しないといけない。
フォームにコントロールとして配置する場合は、Me.Controls.Add(ProgID)
を使用する。
WindowsMediaPlayerのProgIDはWMPlayer.OCX.7
なので、ここではこれで動作した。当然CLSIDの方は使ってもエラーだった。
ただし、レイトバインディングでは型がObject
となるし、WithEvents
を使ったイベントハンドラを作る事ができないのには注意する必要がある。
'OK Dim wmp As Object Set wmp = Me.Controls.Add("WMPlayer.OCX.7") 'NG Public WithEvents wmp As Object Set wmp = Me.Controls.Add("WMPlayer.OCX.7")
WindowsMediaPlayerのプロパティ
WMPのプロパティはあまり数が多くない。が、全ての解説を書くには多すぎるので、他を当たって欲しい。
割と最近のVBAの仕様例
https://e-vba.com/windowsmediaplayer/
VB時代の奴
http://hanatyan.sakura.ne.jp/vb6/sound04.htm
言語はVBAでないが、情報量としては一番多そう
https://so-zou.jp/software/tech/programming/c-sharp/media/video/ax-windows-media-player/
今回の記事でよく使うプロパティ
uiMode = "none"
WMPの各種ボタンが消える
Visible = False
WMPのコントロールが非表示になる。今回の検証で"uiMode"を変更した時にWidth
とHeight
が連動して変化しなくなることが判明した。
課題1:モジュール破損のトラブル
ここからは、A氏とのやり取りの中で発生した課題と、解決するまでの詳細を紹介する。
起きたこと
実際にあった出来事を時系列で記載する。
- A氏が(ActiveXで)WMPを使ったExcelを公開
- 私も含む多くの人が「メモリ不足です」等のエラーが出て動かない事を訴える。
- フォームのコードは開けるが、デザインモードが開けない状態になっていた。
- モジュールをエクスポートしようとすると、上記「メモリ不足です」が出てしまった。
- 参照設定を確認するとWMPは参照不可にはなっておらず、チェックも外れていなかった。
- 別のフォームを作ってそこにWMPを配置してみたら問題なく配置できたし、正しく動作した。
- その後ブックを保存して開き直したが、相変わらずもともとあったモジュールは「メモリ不足です」となる。
- コードだけを新しく作ったフォームにコピペして、必要なコントロールを適当に配置したところ、正常に動作した。
考えたこと
- 犯人は誰?
- 原因の所在はどこ?
- エクセルブックそのもの?
- モジュールのコードからは触れない部分?
- VBAプロジェクトに記憶されたPコード等の部分?
- どうすれば治る?
- ブックを作り直せるか。
- VBADecompilerを使ったらどうなるか。
- モジュールをエクスポートした状態で配布してもらい、各自でインポートするとどうなるのか。
- WMPを動的に配置するようにすれば、環境依存のトラブルは起こらないのではないか。
試してみた
ブックを作り直せるか。
- モジュールがエクスポートできない。
- モジュールをドラッグして別のプロジェクトに動かせない。
よって無理。
VBADecompilerを使ったらどうなるか。
- VBAはPコードという中間言語的なものに翻訳されてから実行されている。
- Pコードは、VBAProject.binの中にコンパイルした時の情報が保持されており、たまにバグる。
- VBAのPコードについては、AddinBox - Tips13: VBA は、インタープリタか? コンパイラか? を読むと良い。
- Pコードを削除してブックを軽くするツール VBA CODE DECOMPILER AND COMPACTOR というものが有る。
結果はエクスポートと同様に、エラーが連発してツールが正常に実行されなかった。
モジュールを各自でインポートするとどうなるのか。
VBAはfrmファイルをプロジェクトにドラッグ&ドロップでインポートできる。
すると・・・
--------------------------- Microsoft Visual Basic for Applications --------------------------- 読み込み中にエラーが発生しました。詳細は 'C:\Users\hogehoge\TypingEX - 0827配布用\ソースコード\フォーム\GameForm.log' を参照してください。 --------------------------- OK ヘルプ --------------------------- --------------------------- Microsoft Visual Basic for Applications --------------------------- システム エラーです: &H80004005 (-2147467259) エラーを特定できません --------------------------- OK ヘルプ --------------------------- --------------------------- Microsoft Visual Basic for Applications --------------------------- メモリが不足しています。 --------------------------- OK ヘルプ ---------------------------
また、例のエラーの嵐が出た。
つまり、フォームモジュールの中に元凶が生きているということだ。
log
を見ろとのことなので、見ると・・・
行 2: プロパティ OleObjectBlob(GameForm) が設定できません。
とのこと。
OLEオブジェクトとのことなので、WMPの可能性が一段と高まった。
WMPを動的に配置すれば良いのではないか。
先に示したMe.Controls.Add
で、WMPを動的に配置することが可能。
Set wmp = Me.Controls.Add("WMPlayer.OCX.7")
これで、参照設定を解除することができ、環境依存の問題も解決した。
ところが・・・
課題2:プロパティ操作のトラブル
実行時バインディングに変えたことで、プロパティの初期値もVBAコードで設定することになった。
起きたこと
以下のような方法でプロパティの変更を行ったところ、サイズ変更が無視されてしまう現象が発生した。
'wmpのプロパティを色々変更 With wmp .enableContextMenu = False '右クリックメニューの無効化 .fullScreen = False '全画面表示を無効化 .stretchToFit = True 'ウィンドウに合わせるか .uiMode = "none" '一番シンプルに .windowlessVideo = False .Top = 20 'コントロールの上端 .Left = 20 'コントロールの左端 .Width = 192 'コントロールの幅 .Height = 108 'コントロールの高さ End With
UserForm_Activateイベントで処理した場合(意図しないサイズ)
A氏が調査した結果、「UserForm_Activate
やUserForm_Initialize
でのサイズ変更は無視されるので、WMPのイベント処理中にやれば良い。」という事を突き止めた。
wmp_StatusChangeイベントで処理した場合(正常なサイズ)
しかし、イベント処理をおこなうWithEvents句はアーリーバインディングでなければ使用することができないので、課題1が再浮上してしまった。
検証してみた
そこで、私は「何故StatusChange
イベントを使えば大丈夫なのか」を突き止めるため、じっくりとデバッグしてみた。
ソースコード抜粋
'フォームが表示された Private Sub UserForm_Activate() Debug.Print "UserForm_Activate - Start" Set wmp = Me.Controls.Add("WMPlayer.OCX.7") With wmp Debug.Print ".URL=[path]" .Url = ThisWorkbook.Path & "\Data\This game\Video\This game.mp4" Debug.Print ".Controls.Play" .Controls.Play '再生開始 End With Debug.Print "無限ループ" '無限ループ(省略) Debug.Print "UserForm_Activate - End" End Sub 'WMPのstatusプロパティの値が変更された Private Sub wmp_StatusChange() Debug.Print "wmp_StatusChange - Start" 'wmpのプロパティを色々変更 Debug.Print "wmp_StatusChange - End" End Sub
実行結果(正常にサイズ変更された)
UserForm_Activate - Start .URL=[path] wmp_StatusChange - Start wmp_StatusChange - End 'Start,Endを更に4回繰り返し .Controls.Play
少し気になったので、2回目以降のwmp_StatusChange
を無効化してみた。
ソースコード抜粋
Private Sub wmp_StatusChange() Static first As Boolean If Not first Then first = True Debug.Print "wmp_StatusChange - Start" 'wmpのプロパティを色々変更 Debug.Print "wmp_StatusChange - End" End If End Sub
実行結果(サイズ変更されなかった)
UserForm_Activate - Start .URL=[path] wmp_StatusChange - Start wmp_StatusChange - End .Controls.Play
要するに、wmp_StatusChange
が繰り返し実行された事による遅延実行が成功のカギである可能性が高い。
その後、ソースコードは省略するが、wmp_StatusChange
イベントを消して、Application.OnTime
を使って少しづつ遅延させるタイミングを変えて行ったところ、
.uiMode = "none" '一番シンプルに
と
.Top = 20 'コントロールの上端 .Left = 20 'コントロールの左端 .Width = 192 'コントロールの幅 .Height = 108 'コントロールの高さ
の間に一旦時間差を設ければ良い事が分かった。
これで問題2は解決である。
※ちなみに、この2つの順番を入れ替えても効果はない。
これが答えだ!と、思ったのだが
ちゃぶ台返しされた
上記の方法では、コントロールが変形する時に画面がチラつくのが気になった。
そこで、事前にwmp.Visible = False
で非表示にして、プロパティ変更が終わった後にwmp.Visible = True
で表示するようにしてみた。
すると・・・遅延処理が必要なくなった。のである。
つまり、以下のようなプログラムで良い事がわかった。
'フォームが表示された Private Sub UserForm_Activate() Debug.Print "UserForm_Activate - Start" Set wmp = Me.Controls.Add("WMPlayer.OCX.7") With wmp .Visible = False 'WMPを非表示にする Debug.Print ".URL=[path]" .Url = ThisWorkbook.Path & "\Data\This game\Video\This game.mp4" Debug.Print ".Controls.Play" .Controls.Play '再生開始 .enableContextMenu = False '右クリックメニューの無効化 .fullScreen = False '全画面表示を無効化 .stretchToFit = True 'ウィンドウに合わせるか .uiMode = "none" '一番シンプルに .windowlessVideo = False 'ウィンドウなしでビデオを表示 .Top = 20 'コントロールの上端 .Left = 20 'コントロールの左端 .Width = 192 'コントロールの幅 .Height = 108 'コントロールの高さ .Visible = True 'WMPを表示する End With Debug.Print "無限ループ" Do While Is_GameEnd = False '処理 DoEvents Sleep 10 Loop Debug.Print "UserForm_Activate - End" End Sub
仮説を立ててみる
さて、このような結果になった理由を、勝手に予想してみる。
そもそもの原因となったuiMode
プロパティであるが、これはWindowsMediaPlayerのスタイルを決めるプロパティであるため、値が変化した時にサイズが変更されるのではないだろうか。
さらに、そのサイズ変更はVBAと非同期で行われるのではないだろうか。
その結果がこれである。
しかしVisible = False
で非表示の間にuiMode
を変更した場合はサイズ変更が行われないのだろう。
実際に動かしてみると、確かに16:9
ではない中途半端なサイズになっている。
つまり、非表示にしない場合はこんな流れ
非表示にしている間に処理した場合はこんな流れ
真のWMPの実装は分からないが、実際の動きを見た限りそういうことなのだろう。
まとめ
長くなってしまったが、今回の教訓は以下の2点だ。
ActiveXコントロールを使う時は、参照設定を使うべきではない。
- できるだけ、動的に配置(
Me.Controls.Add("WMPlayer.OCX.7")
)しよう。 - やむを得ず使うとしたらイベント処理をしたい場合に限られる。
- 開発中は使ったほうが効率的だが、配布前に必ず切断しよう。
- できるだけ、動的に配置(
ActiveXコントロールは、非同期で悪さする可能性が有ることを意識しよう。
- VBAで記述する順番に気をつける。
- プロパティの変更ひとつひとつに気を配って、非同期のイベントを開発者の期待したタイミングで処理させる。
- 今回のようにイベント自体の負担を抑えることで解決するかもしれない。
以上
何か御座いましたらコメント欄、またはTwitterからどうぞ♪
それではまた来週♪ ちゅんちゅん(・8・)