えくせるちゅんちゅん

ことりがエクセルをちゅんちゅんするブログ

VBAで環境に依存しないWindowsMediaPlayerの使い方

先日、フォロワーの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に任せる


VBAWMPを使う

VBAで外部コンポーネントを使うには、アーリーバインディング(事前バインディング)とレイトバインディング(実行時バインディング)の2種類の選択肢がある。

2つの違いを簡単に言うと、予め型をはっきりさせてコンパイルチェック等もしっかりした上で実行するか、Object型変数で型が不明なまま実行して、実際にインスタンスを作成する場面にたどり着いて初めて型が確定する。という感じである。

下手な説明で申し訳ないが、これらの語句で検索すれば誰かが上手に説明してくれているはず。

それと、CLSID(クラスID)やProgIDという言葉が出てくるが、とても詳しく解説してくれている記事を教えてもらったので追記しておく。

WSHのCreateObject関数の引数のCOM識別子「ProgID」「CLSID」(GUID)とは何なのか解説。Windows内のActiveXオブジェクトを一覧表示して確認するコマンド


アーリーバインディング(事前バインディング

一番オーソドックスなのは、参照設定して使うことだろう。

ところが、参照設定を開くとWindows Media Playerというライブラリが2つ出てくるかもしれない。

f:id:Kotori-ChunChun:20190829222112p:plain

それぞれのパスを確認すると以下のようになっている。

C:\WINDOWS\system32\wmp.dll
C:\Windows\System32\msdxm.tlb

wmp.dllが本物のような気がするが、これだけでは確証がない。


実はツールボックスからWMPを設置することで確認できる。(というかこっちのやり方が普通か?)

f:id:Kotori-ChunChun:20190829223130p:plain

つまりC:\WINDOWS\system32\wmp.dllが本物である。

そもそも、コントロールを配置すると、勝手に参照設定が行われるので開発者自身はパスを覚える必要は無い。

あるとすれば、ブックを他のPCに持っていった時に、参照設定が切れてしまった場合にやり直す場面くらいだろうか。


使い方は普通のラベルやボタンと同じように、ツールボックスからGUI上で配置するだけなので説明する必要はないと思う。

f:id:Kotori-ChunChun:20190829223452p:plain

もちろん、プロパティウィンドウで初期値を変更することができる。


コードから動的に生成しようかと思ったのだが、エラーが出てしまった。

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")

また、アーリーバインディングで宣言した変数はインテリセンスが働くので便利に使える。

f:id:Kotori-ChunChun:20190829223911p:plain


メリットをまとめると

などである。


一方でデメリットは

  • 参照設定が環境によって切れる
  • Excelブックを開くたびに警告が出る
  • ふとした拍子に参照設定が壊れる。運が悪いとブックが開けなくなりオダブツも

等と、少々怖い問題が挙げられる。


参照設定が環境によって切れる

f:id:Kotori-ChunChun:20190830212653p:plain

Excelブックを開くたびに警告が出る

f:id:Kotori-ChunChun:20190830211120p:plain


Microsoft Forms

このアプリケーションは、安全でない可能性のある ActiveX コントロールを初期化しようとしています。このファイルの提供元が信頼できる場合は、[OK] をクリックします。コントロールは現在のワークスペースの設定で初期化されます。


OK キャンセル

ふとした拍子に参照設定が壊れる

上記警告をOKしても以下のようなエラーが出る。

f:id:Kotori-ChunChun:20190830211251p:plain


Microsoft Visual Basic for Applications

システム エラーです: &H80004005 (-2147467259) エラーを特定できません

OK ヘルプ

f:id:Kotori-ChunChun:20190830211355p:plain


Microsoft Visual Basic for Applications

メモリが不足しています。

OK ヘルプ

フォームをデザインモードで開こうとしても出る。

f:id:Kotori-ChunChun:20190830211431p:plain

もしこんな状態になってしまった場合、フォームのデザイン情報を復活させる事はできるのだろうか?


レイトバインディング(実行時バインディング

もう一つの方法が、Object型変数にCreateObjectで生成したインスタンスを記憶させる方法。

'いずれも動かない
Dim wmp As Object 
Set wmp = CreateObject("WMPlayer.OCX")
Set wmp = CreateObject("WMPlayer.OCX.7")

最後の.7は付けたり消したり。情報が錯綜しており、どちらにすべきかよく分からない。

最初は動いていた気がするのだが、今では何度試しても以下のエラーが出るため動かない。

実行時エラー '-2147467259 (80004005)':
オートメーション エラーです。
エラーを特定できません 

f:id:Kotori-ChunChun:20190829225302p:plain


調べていたら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のプロパティはあまり数が多くない。が、全ての解説を書くには多すぎるので、他を当たって欲しい。

Google検索


割と最近の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"を変更した時にWidthHeightが連動して変化しなくなることが判明した。


課題1:モジュール破損のトラブル

ここからは、A氏とのやり取りの中で発生した課題と、解決するまでの詳細を紹介する。

起きたこと

実際にあった出来事を時系列で記載する。

  1. A氏が(ActiveXで)WMPを使ったExcelを公開
  2. 私も含む多くの人が「メモリ不足です」等のエラーが出て動かない事を訴える。
  3. フォームのコードは開けるが、デザインモードが開けない状態になっていた。
  4. モジュールをエクスポートしようとすると、上記「メモリ不足です」が出てしまった。
  5. 参照設定を確認するとWMPは参照不可にはなっておらず、チェックも外れていなかった。
  6. 別のフォームを作ってそこにWMPを配置してみたら問題なく配置できたし、正しく動作した。
  7. その後ブックを保存して開き直したが、相変わらずもともとあったモジュールは「メモリ不足です」となる。
  8. コードだけを新しく作ったフォームにコピペして、必要なコントロールを適当に配置したところ、正常に動作した。


考えたこと

  • 犯人は誰?
  • 原因の所在はどこ?
    • エクセルブックそのもの?
    • モジュールのコードからは触れない部分?
    • VBAプロジェクトに記憶されたPコード等の部分?
  • どうすれば治る?
    • ブックを作り直せるか。
    • VBADecompilerを使ったらどうなるか。
    • モジュールをエクスポートした状態で配布してもらい、各自でインポートするとどうなるのか。
    • WMPを動的に配置するようにすれば、環境依存のトラブルは起こらないのではないか。


試してみた

ブックを作り直せるか。

  • モジュールがエクスポートできない。
  • モジュールをドラッグして別のプロジェクトに動かせない。

よって無理。


VBADecompilerを使ったらどうなるか。

結果はエクスポートと同様に、エラーが連発してツールが正常に実行されなかった。


モジュールを各自でインポートするとどうなるのか。

VBAはfrmファイルをプロジェクトにドラッグ&ドロップでインポートできる。

f:id:Kotori-ChunChun:20190831115645p:plain

すると・・・

f:id:Kotori-ChunChun:20190830211120p:plain

---------------------------
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イベントで処理した場合(意図しないサイズ)

f:id:Kotori-ChunChun:20190831205558p:plain


A氏が調査した結果、「UserForm_ActivateUserForm_Initializeでのサイズ変更は無視されるので、WMPのイベント処理中にやれば良い。」という事を突き止めた。

wmp_StatusChangeイベントで処理した場合(正常なサイズ)

f:id:Kotori-ChunChun:20190831205506p:plain

しかし、イベント処理をおこなう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

実行結果(正常にサイズ変更された)

f:id:Kotori-ChunChun:20190831205506p:plain

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

実行結果(サイズ変更されなかった)

f:id:Kotori-ChunChun:20190831205558p:plain

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と非同期で行われるのではないだろうか。

その結果がこれである。

f:id:Kotori-ChunChun:20190831205558p:plain


しかしVisible = Falseで非表示の間にuiModeを変更した場合はサイズ変更が行われないのだろう。

実際に動かしてみると、確かに16:9ではない中途半端なサイズになっている。

f:id:Kotori-ChunChun:20190831214650p:plain


つまり、非表示にしない場合はこんな流れ

f:id:Kotori-ChunChun:20190831214117p:plain

非表示にしている間に処理した場合はこんな流れ

f:id:Kotori-ChunChun:20190831215414p:plain


真のWMPの実装は分からないが、実際の動きを見た限りそういうことなのだろう。


まとめ

長くなってしまったが、今回の教訓は以下の2点だ。

  • ActiveXコントロールを使う時は、参照設定を使うべきではない。

    • できるだけ、動的に配置(Me.Controls.Add("WMPlayer.OCX.7"))しよう。
    • やむを得ず使うとしたらイベント処理をしたい場合に限られる。
    • 開発中は使ったほうが効率的だが、配布前に必ず切断しよう。
  • ActiveXコントロールは、非同期で悪さする可能性が有ることを意識しよう。

    • VBAで記述する順番に気をつける。
    • プロパティの変更ひとつひとつに気を配って、非同期のイベントを開発者の期待したタイミングで処理させる。
    • 今回のようにイベント自体の負担を抑えることで解決するかもしれない。

以上


何か御座いましたらコメント欄、またはTwitterからどうぞ♪

それではまた来週♪ ちゅんちゅん(・8・)