今回はこのプログラムの核心、DirectShowのクラスCDSHOWを(解説:)します。(注)
注:このブログをアップした後、DirectShowのエラー発生時対応を改良した為、以下のファイルを2か所修正しました。一つはGetDuration()関数にREFTIMEのローカス変数を使っていましたが、メンバー変数のm_Lenにしたこと(というか、完全に忘れていました、この変数!)、もう一つはIsOver()関数が真偽(bool)の戻り値でしたが、正確な終了自由を取りたいユーザーがいるはずなので「参照型long引数(long&)」を与え、IsOver(evnt)のように使うことで、再生終了、再生中止、再生エラーの終了事由を取ることができるようにしました。以下は既に修正されています。
【CDSHOW.h】
///////////////////////////
//CDSHOWクラス定義ファイル
///////////////////////////
#include <DShow.h> //DirectShowヘッダー
//(解説:DirectShowを使う場合には、このヘッダーファイルを必ず読み込みます。)
#include <wchar.h>
//(解説:COMの文字列はワイド文字が標準であり、wchar.hは必ず読み込みます。)
//フィルター定義(利用者のフィルターの環境によって適宜変更が必要)
#define FILTER "ビデオファイル(*.avi;*.mp4;*.mov)\0*.avi;*.mp4;*.mov\0オーディオファイル(*.mp3;*.wav;*.mid)\0*.mp3;*.wav;*.mid\0\0"
//(解説:これは私の利用したいファイル種類を列挙しただけで、DirectShowはこれ以上の対応幅があります。)
//親ウィンドウへの通知メッセージ
#define WM_GRAPHNOTIFY WM_APP + 1
//(解説:ユーザー定義メッセージです。)
class CDSHOW {
protected:
//DirectShow親ウィンドウ
HWND m_hWnd;
//DirectShow関連変数
IGraphBuilder *m_pGraph; //フィルターグラフビルダークラスポインター
IMediaControl *m_pControl; //メディアコントロールインターフェースポインター
IMediaEventEx *m_pEvent; //メディアイベントインターフェースポインター
IVideoWindow *m_pVideoWindow; //ビデオウィンドウポインター
IMediaPosition *m_pMPos; //メディア再生位置インターフェースポインター
//結果判定用変数
HRESULT m_hr;
//(解説:内部で処理する関連の変数なのでprotectedとしました。)
public:
//ファイル名取得用変数
char m_FileName[MAX_PATH];
WCHAR m_WFileName[MAX_PATH];
//(解説:ファイル名はユーザーからアクセスすることを許します。通常はマルチバイトのchar*を使います。)
//ファイル再生時間変数(double)
REFTIME m_Len;
//(解説:再生時間取得用で、REFTIMEはd実際はdoubleです。)
//メンバー関数
CDSHOW(); //コンストラクター
~CDSHOW(); //デストラクター
void Init(); //メンバー関数の初期化
bool Move(int, int, int, int); //ビデオウィンドウの位置設定(x, y, w, h)
bool SetFileName(char*, char*); //ビデオファイル名を設定する
bool Show(HWND); //ビデオウィンドウの初期化とビデオの再生
REFTIME GetDuration(); //ビデオ再生時間を返す
OAFilterState GetState(int); //フィルターグラフの再生状態を取得(0-Stopped、1-Paused、2-Running)
bool Pause(); //ビデオの一時停止
bool Continue(); //ビデオの再開
void Stop(); //ビデオの中止
REFTIME GetCurrentPos(); //現在の再生時間を返す
bool SetCurrentPos(REFTIME); //再生位置(時間-秒)を設定する
bool IsOver(long&); //親ウィンドウに置く終了モニタリング関数
void CleanUp(); //ビデオウィンドウ、フィルターグラフの終了処理
};
//コンストラクター
CDSHOW::CDSHOW() {
//ワイド文字使用のためロケールの初期化(日本語)
setlocale(LC_CTYPE, "JPN");
//メンバー変数の初期化
Init();
//COMの初期化
m_hr = CoInitialize(NULL);
if(FAILED(m_hr))
MessageBox(m_hWnd, "COMを初期化できませんでした", "エラー", MB_OK | MB_ICONERROR);
//(解説:マルチバイト文字のワイド文字変換にはロケールが不可欠です。メンバー変数の初期化はInit関数にまとめ、一回だけ行うCOM初期化も実行します。)
}
//デストラクター
CDSHOW::~CDSHOW() {
//フィルターグラフの終了処理
CleanUp();
//(解説:メモリー解放を行うべきものはCleanUpで纏めて実施します。)
//COMの終了処理
CoUninitialize();
//(解説:COMの必須終了処理です。)
}
//メンバー関数の初期化
void CDSHOW::Init() {
//親ウィンド ウハンドル
m_hWnd = NULL;
//DirectShow関連変数
m_pGraph = NULL;
m_pControl = NULL;
m_pEvent = NULL;
m_pVideoWindow = NULL;
m_pMPos = NULL;
m_Len = 0;
//(解説:メンバー変数の初期化です。再生のたびに実施します。)
}
//ビデオウィンドウの位置設定
bool CDSHOW::Move(int x, int y, int w, int h) {
//ビデオウィンドウが無ければ何もしない
if(!m_pVideoWindow)
return FALSE;
if(m_pVideoWindow) {
//描画先ウィンドウ内の描画位置の設定
m_hr = m_pVideoWindow->SetWindowPosition(x, y, w, h);
if(m_hr != S_OK && m_hr != E_NOINTERFACE) { //VFW_E_NOT_CONNECTEDではなく、E_NOINTERFACEが返される
//(解説:例の「画像フィルターが不要でビデオウィンドウが作れてしまう場合」の仕様外のエラーメッセージが出る件です。)
MessageBox(m_hWnd, "ビデオウィンドウの位置を設定できませんでした", "エラー", MB_OK | MB_ICONERROR);
return FALSE;
}
return TRUE;
}
else
return FALSE;
}
//ビデオファイル名を設定する
bool CDSHOW::SetFileName(char* fn, char* ftr = FILTER) {
//(解説:DirectShowに読ませるワイド文字ファイルパス、名です。マルチバイト文字のファイルパス、名を変換します。)
if(*fn) {
//(解説:ファイル名指定がある場合です。)
if(lstrlen(fn) > MAX_PATH - 1) {
MessageBox(m_hWnd, "ファイルパス、名またはURLが長すぎます", "エラー", MB_OK | MB_ICONERROR);
*m_FileName = NULL;
*m_WFileName = L'\0'; //16bitの終端文字
return FALSE;
}
else {
//ファイル名の記録
lstrcpy(m_FileName, fn);
//ファイル名のワイド文字化
mbstowcs(m_WFileName, m_FileName, MAX_PATH);
return TRUE;
}
}
else {
//(解説:ファイル名指定が無い("")場合です。cmndlgを使わずWin32APIのGetOpenFileNameで書いてみました。)
OPENFILENAME ofn; //オープンファイルダイアログ構造体
ZeroMemory(&ofn, sizeof(ofn)); //ゼロ初期化
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = m_hWnd;
ofn.lpstrFilter = ftr; //引数でフィルタリングする
ofn.nFilterIndex = 1;
ofn.lpstrFile = m_FileName;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrTitle = NULL;
ofn.lpstrInitialDir = ".";
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT;
ofn.lpstrDefExt = NULL;
bool success = GetOpenFileName(&ofn);
if(success) {
//ファイル名のワイド文字化
mbstowcs(m_WFileName, m_FileName, MAX_PATH);
return TRUE;
}
else {
*m_FileName = NULL;
*m_WFileName = L'\0'; //16bitの終端文字
return FALSE;
}
}
}
//ビデオウィンドウの初期化とビデオの再生
bool CDSHOW::Show(HWND hWnd) {
//(解説:ワイド文字ファイルパス、名をセットし、再生の都度インスタンスを作成し、終了時に開放します。)
//親ウィンドウの記録
m_hWnd = hWnd;
//フィルターグラフマネージャーを作成し、IGraphBuilderインターフェイスを取得
m_hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&m_pGraph);
if(FAILED(m_hr)) {
MessageBox(m_hWnd, "フィルターグラフマネージャーを作成できませんでした",
"エラー", MB_OK | MB_ICONERROR);
goto exit;
//(解説:多くのチェックがあるのでエラー処理をまとめて、久々のgotoを使いました。。)
}
//IGraphBuilderからIMediaControlインターフェイスを取得
m_hr = m_pGraph->QueryInterface(IID_IMediaControl, (void **)&m_pControl);
if(FAILED(m_hr)) {
MessageBox(m_hWnd, "メディアコントロールインターフェースを取得できませんでした",
"エラー", MB_OK | MB_ICONERROR);
goto exit;
}
//IGraphBuilderからIMediaEventインターフェイスを取得
m_hr = m_pGraph->QueryInterface(IID_IMediaEvent, (void **)&m_pEvent);
if(FAILED(m_hr)) {
MessageBox(m_hWnd, "メディアイベントインターフェースを取得できませんでした",
"エラー", MB_OK | MB_ICONERROR);
goto exit;
}
//指定されたファイルを再生できるフィルターグラフを作成する(存在しない場合、エラー)
m_hr = m_pGraph->RenderFile(m_WFileName, NULL);
if(FAILED(m_hr)) {
MessageBox(m_hWnd, "フィルターグラフを作成できませんでした", "エラー", MB_OK | MB_ICONERROR);
goto exit;
}
//(解説:特定のPC環境で再生できるフィルターが無い場合、ここでエラーとなります。)
//再生位置インターフェースを取得する
m_pGraph->QueryInterface(IID_IMediaPosition, (LPVOID *)&m_pMPos);
//(解説:再生状態の音とロールに必要です。)
//動画ファイルの長さ(秒-double)を取得
m_pMPos->get_Duration(&m_Len);
//(解説:ファイルの再生時間(管理上の分母)を取得します。)
//親ウィンドウへの通知を開始する
m_pEvent->SetNotifyWindow((OAHWND)m_hWnd, WM_GRAPHNOTIFY, 0);
//(解説:イベントメッセージを親に転送し、状態を把握します。以上は再生状況監視、コントロール用です。)
//ビデオコントロールを取得する
m_hr = m_pGraph->QueryInterface(IID_IVideoWindow, (LPVOID*)&m_pVideoWindow);
if(FAILED(m_hr)) {
MessageBox(m_hWnd, "ビデオウィンドウを取得できませんでした", "エラー", MB_OK | MB_ICONERROR);
goto exit;
}
//親ウィンドウの設定
m_hr = m_pVideoWindow->put_Owner((OAHWND)m_hWnd);
if(m_hr != S_OK && m_hr != E_NOINTERFACE) { //VFW_E_NOT_CONNECTEDではなく、E_NOINTERFACEが返される
//(解説:以降は例の「画像フィルターが不要でビデオウィンドウが作れてしまう場合」の仕様外のエラーメッセージが出る件です。FAILED()マクロは使えません。)
MessageBox(m_hWnd, _T("親ウィンドウを設定できませんでした"), _T("エラー"), MB_OK | MB_ICONERROR);
goto exit;
}
//ウィンドウスタイルを設定する
m_hr = m_pVideoWindow->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS);
if(m_hr != S_OK && m_hr != E_NOINTERFACE) { //VFW_E_NOT_CONNECTEDではなく、E_NOINTERFACEが返される
MessageBox(m_hWnd, "ウィンドウスタイルを設定できませんでした", "エラー", MB_OK | MB_ICONERROR);
goto exit;
}
//描画先ウィンドウを640 x 480の表示が行える最小サイズにする
SetWindowPos(m_hWnd, HWND_TOP, 0, 0, 656, 541, SWP_NOMOVE);
//(解説:クライアント領域サイズ640x480のウィンドウサイズはPCによって異なる可能性があります。)
//サイズ変更が無い場合の為に親ウィンドウへWM_SIZEを送る
SendMessage(m_hWnd, WM_SIZE, 0, (LPARAM)MAKELONG(640, 480));
//(解説:既に最小サイズになっている場合、SetWindowPosではWM_SIZEが出ないので、強制的に出します。)
//指定したウィンドウへIVideoWindowの通知を流す
m_hr = m_pVideoWindow->put_MessageDrain((OAHWND)m_hWnd);
//(解説:この再生中、ビデオウィンドウの通知は親に流されます。マウスクリックやキー入力などで必要です。)
//ウィンドウを表示する
m_hr = m_pVideoWindow->put_Visible(OATRUE);
if(m_hr != S_OK && m_hr != E_NOINTERFACE) { //VFW_E_NOT_CONNECTEDではなく、E_NOINTERFACEが返される
MessageBox(m_hWnd, "ビデオウィンドウを表示できませんでした", "エラー", MB_OK | MB_ICONERROR);
goto exit;
}
//動画表示を開始する。
m_hr = m_pControl->Run();
if(SUCCEEDED(m_hr))
return TRUE;
//以上が正常終了で、エラーがあると以下を実行する
exit:
CleanUp();
//(解説:終了処理です。)
return FALSE;
}
//ビデオ再生時間を返す
REFTIME CDSHOW::GetDuration() {
m_pMPos->get_Duration(&m_Len);
return m_Len;
//(解説:再生時間(秒単位)を返します。)
}
//フィルターグラフの再生状態を取得(0-Stopped、1-Paused、2-Running)
OAFilterState CDSHOW::GetState(int delay) {
OAFilterState fs = 0;
if(m_pControl)
m_pControl->GetState(delay, &fs); //処理遅延の為、delay(ミリ秒)後に取得
//(解説:再生終了を監視する目的で使い、delayミリ秒後の、コメントに書かれた状態を返します。ビデオコントロールインスタンスが無いと落ちますので注意してください。)
return fs;
}
//ビデオの一時停止
bool CDSHOW::Pause() {
//動画表示を一時停止する。
m_hr = m_pControl->Pause();
if(SUCCEEDED(m_hr))
return TRUE;
else
return FALSE;
//(解説:これもビデオコントロールインスタンスが無いと落ちますが、メインウィンドウでメニュー、ボタン状態を有無効処理をしてしのいでいます。)
}
//ビデオの再開
bool CDSHOW::Continue() {
//動画表示を一時停止する。
m_hr = m_pControl->Run();
if(SUCCEEDED(m_hr))
return TRUE;
else
return FALSE;
}
//ビデオの中止
void CDSHOW::Stop() {
//動画表示を中止し、終了処理を行う
CleanUp();
}
//(解説:再生、一時停止、中止のコントロールに使います。)
//現在の再生時間を返す
REFTIME CDSHOW::GetCurrentPos() {
REFTIME tm;
m_pMPos->get_CurrentPosition(&tm);
return tm;
}
//(解説:経過時間、残再生時間の取得の為に使います。)
//再生位置(時間-秒)を設定する
bool CDSHOW::SetCurrentPos(REFTIME tm) {
m_pMPos->put_CurrentPosition(tm);
}
//(解説:スライダーで元に戻したり、先に進める為に使います。)
//親ウィンドウに置く終了モニタリング関数
bool CDSHOW::IsOver(long& evtCode) {
//IMediaEvent *m_pEventが設定されていなければFALSEで返る
if(!m_pEvent)
return FALSE;
//(解説:これもインスタンスが無いのに実行すると落ちます。)
//イベント処理
LONG_PTR Param1, Param2;
while(SUCCEEDED(m_pEvent->GetEvent(&evtCode, &Param1, &Param2, 0))) {
m_pEvent->FreeEventParams(evtCode, Param1, Param2);
switch(evtCode) {
case EC_COMPLETE: //再生終了
case EC_USERABORT: //再生中止
case EC_ERRORABORT: //エラー
CleanUp();
return TRUE;
}
}
return FALSE;
//(解説:WaitForCompletion関数を使うとウィンドウが無効化するので使わず、ユーザー定義のWM_GRAPHNOTIFYメッセージを親に送ってこの関数で監視します。)
}
//ビデオウィンドウ、フィルターグラフの終了処理
void CDSHOW::CleanUp() {
//メモリーの開放
if(m_pVideoWindow) {
//表示ウィンドウを消す
//m_pVideoWindow->put_Visible(OAFALSE);
//親をデスクトップとする
//m_pVideoWindow->put_Owner(NULL);
m_pVideoWindow->Release();
}
if(m_pMPos)
m_pMPos->Release();
if(m_pControl) {
//動画表示を中止する。
m_pControl->Stop();
m_pControl->Release();
}
if(m_pEvent) {
//親ウィンドウへの通知を終了する
m_pEvent->SetNotifyWindow((OAHWND)NULL, 0, 0);
m_pEvent->Release();
}
if(m_pGraph)
m_pGraph->Release();
//再初期化
Init();
//(解説:「後入れ先出し」で、先にインターフェースからメモリーを解放して、親玉のグラフマネージャーを最後に開放します。全て解放したら、メンバー変数を初期化します。)
}
DirectShowは必要な処理をすべてメソッドで纏められているので、ユーザーのラッピングは非常に簡単です。これでメインウィンドウの処理が簡素化できます。
【CSLIDER.h】
/////////////////////////////////////////
//スライダー(CSLIDER)クラス定義ファイル
/////////////////////////////////////////
class CSLIDER : public CCTRL
{
public:
//メンバー関数
bool Create(HWND, int, int,
int, int, int, DWORD); //コントロール作成関数
void AutoSize(int, int, int); //親のサイズ変更に合わせて位置(0-3)を変更
void SetSize(int, int, int, int); //位置と大きさを変更する
void SetRange(int, int); //スライダーの範囲設定
void SetPageSize(int); //ページサイズ(マウスクリック時の移動量)設定
void SetLineSize(int); //ラインサイズ(矢印キーによる移動量)設定
int GetPosition(); //ポジションの取得
void SetPosition(int); //ポジションの設定
void SetTic(int); //目盛りサイズの設定
};
//(解説:大体何をするのかはコメントでわかると思います。)
//コントロール作成関数
bool CSLIDER::Create(HWND hParent, int cID, int x, int y, int w, int h,
DWORD Style = WS_CHILD | WS_VISIBLE | TBS_AUTOTICKS) {
m_hWnd = CreateWindowEx(m_dwExStyle = WS_EX_TOPMOST,
m_lpClassName = TRACKBAR_CLASS,
m_lpWindowName = "",
m_dwStyle = Style,
m_x = x,
m_y = y,
m_Width = w,
m_Height = h,
m_hParent = hParent,
m_hMenu = (HMENU)cID,
m_hInstance = (HINSTANCE)GetWindowLong(hParent, GWL_HINSTANCE),
m_lpParam = NULL
);
if(m_hWnd == NULL) {
MessageBox(0, "スライダーコントロールを作れませんでした", "エラー",
MB_OK | MB_ICONEXCLAMATION);
return FALSE;
}
//コントロールの表示と再描画
ShowWindow(m_hWnd, SW_SHOWNORMAL);
UpdateWindow(m_hWnd);
return TRUE;
//(解説:スライダーコントロールの作成関数です。)
}
//親のサイズ変更に合わせて位置(時計回りに0-3の隅)を変更
void CSLIDER::AutoSize(int pos, int marginx = 0, int marginy = 0) {
RECT rec;
GetClientRect(m_hParent, &rec);
switch(pos) {
case 0: //左上隅
m_x = marginx;
m_y = marginy;
break;
case 1: //右上隅
m_x = (rec.right - rec.left) - m_Width - marginx;
m_y = marginy;
break;
case 2: //右下隅
m_x = (rec.right - rec.left) - m_Width - marginx;
m_y = (rec.bottom - rec.top) - m_Height - marginy;
break;
case 3: //左下隅
m_x = marginx;
m_y = (rec.bottom - rec.top) - m_Height - marginy;
break;
default:
return;
}
MoveWindow(m_hWnd, m_x, m_y, m_Width, m_Height, TRUE);
//(解説:これは今回ツールバーの中に置くことを考えて作りましたが、他の応用も効くはずです。親ウィンドウの四隅にmarginx, yだけ離してスライダーを置きます。)
}
//位置と大きさを変更する
void CSLIDER::SetSize(int x, int y, int w, int h) {
m_x = x;
m_y = y;
m_Width = w;
m_Height = h;
// SetWindowPos(m_hWnd, HWND_TOP, m_x, m_y, m_Width, m_Height, SWP_SHOWWINDOW);
MoveWindow(m_hWnd, m_x, m_y, m_Width, m_Height, TRUE);
//(解説:AutoSizeでは高さ幅が決められないので、これも併用します。)
}
//スライダーの範囲設定
void CSLIDER::SetRange(int min, int max) {
SendMessage(m_hWnd, TBM_SETRANGE, TRUE, (LPARAM)MAKELONG(min, max));
//(解説:まんま、です。)
}
//ページサイズ(マウスクリック時の移動量)設定
void CSLIDER::SetPageSize(int dst) {
SendMessage(m_hWnd, TBM_SETPAGESIZE, 0, (LPARAM)dst);
//(解説:スライダーノブをつかまない場合の移動量です。)
}
//ラインサイズ(矢印キーによる移動量)設定
void CSLIDER::SetLineSize(int dst) {
SendMessage(m_hWnd, TBM_SETLINESIZE, 0, (LPARAM)dst);
//(解説:キーの場合です。)
}
//ポジションの取得
int CSLIDER::GetPosition() {
return SendMessage(m_hWnd, TBM_GETPOS, 0, 0);
//(解説:スライダーノブを移動させられた場合の位置取得用です。)
}
//ポジションの設定
void CSLIDER::SetPosition(int pos) {
SendMessage(m_hWnd, TBM_SETPOS, TRUE, (LPARAM)pos);
//(解説:処理振興に合わせてスライダーを動かすために使います。)
}
//目盛りサイズの設定
void CSLIDER::SetTic(int tic) {
//目盛りのサイズを設定
SendMessage(m_hWnd, TBM_SETTICFREQ, tic, 0);
//(解説:まんま、です。)
}
以上です。COMの扱いの定石をクラス化で省略することができ、簡単にCOMやスライダーコントロールが使えるようになります。