EternalWindows
WebBrowser コントロール / フィードの実装

今回は、サイドバーに表示されているフィードのウインドウを実装します。 フィードとは、Webサイトの更新情報などを記述したRSSファイルであり、 これを購読することで実際にサイトへアクセスしなくてもサイトの更新を知ることができます。 フィードのウインドウは次のような外観を持ちます。

アイコンが表示されていないために分かりにくいですが、 IEと同じアイテムの数が追加されているはずです。 また、太字で表示されているアイテムは、その関連するページが未読であることを意味します。 右クリックメニューによる削除や名前変更などはサポートしていませんが、 IEなどの外部アプリケーションでそうした動作が行われた場合は検出するようになっています。 フォルダでないアイテムにアクセスした場合は、フィードに関連するページにアクセスします。

フィードのウインドウはCFeedTreeで識別され、CSidebarによって使用されます。 フィードのタブが選択された場合は、CFeedTree::Createが呼ばれます。

BOOL CFeedTree::Create(HWND hwndParent)
{
	HRESULT                   hr;
	IXFeedFolder              *pFeedFolder;
	IConnectionPointContainer *pConnectionPointContainer;

	hr = CoCreateInstance(CLSID_XFeedsManager, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_pFeedsManager));
	if (FAILED(hr))
		return FALSE;
	
	m_hwndFeed = CreateWindowEx(0, WC_TREEVIEW, TEXT(""), WS_CHILD | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT, 0, 0, 0, 0, hwndParent, (HMENU)ID_FEED, NULL, NULL);
	
	m_pFeedsManager->RootFolder(IID_PPV_ARGS(&pFeedFolder));

	pFeedFolder->GetWatcher(FES_ALL, (FEEDS_EVENTS_MASK)(FEM_FEEDEVENTS | FEM_FOLDEREVENTS), IID_PPV_ARGS(&pConnectionPointContainer));

	pConnectionPointContainer->FindConnectionPoint(IID_IXFeedFolderEvents, &m_pConnectionPoint);
	m_pConnectionPoint->Advise(static_cast<IXFeedFolderEvents *>(this), &m_dwCookie);
	
	EnumFolder(TVI_ROOT, pFeedFolder);

	pConnectionPointContainer->Release();
	pFeedFolder->Release();
	
	return TRUE;
}

フィードの管理には、IE7からサポートされているWindows RSS Platformを使用します。 このAPIを使用すればフィードやフォルダを表すインターフェースを取得できますが、 それを表示するためのツリービュー自体は、アプリケーションが作成することになります。 ツリービューを作成したら、IXFeedsManager::RootFolderを呼び出してルートフォルダのIXFeedFolderを取得します。 IXFeedFolderを取得すれば、これを走査することでフィードを取得できますが、 その前にフォルダ内で発生したイベントを検出できるようにしておきます。 このためにはまず、IXFeedFolder::GetWatcherを呼び出してIConnectionPointContainerを取得します。 このとき、第1引数には自分以下の全てのフィードとフォルダを対象にするためにFES_ALLを指定します。 また、第2引数にはFEM_FEEDEVENTSとFEM_FOLDEREVENTSの両方を指定して、 フィードとフォルダのどちらのイベントも検出できるようにしておきます。 IID_IXFeedFolderEventsを指定して接続ポイントを取得したら、 Adviseを呼び出すことによってイベントを受け取るオブジェクトを登録します。 今回の場合このオブジェクトはCFeedTreeであり、IXFeedFolderEventsを実装しておくことになります。

EnumFolderは、フォルダ内のフィードとフォルダを列挙します。 フォルダを見つけた場合は、そのフォルダ以下をさらに列挙するために再帰呼び出しを行います。

void CFeedTree::EnumFolder(HTREEITEM hitem, IXFeedFolder *pFeedFolder)
{
	UINT         i, uCount;
	UINT         uUnreadItemCount;
	LPWSTR       lpszName, lpszUrl, lpszPath;
	IXFeed       *pFeed;
	IXFeedFolder *pFeedSubFolder;
	IXFeedsEnum  *pFeedsEnum;
	HTREEITEM    hitemChild;

	pFeedFolder->Subfolders(&pFeedsEnum);
	pFeedsEnum->Count(&uCount);
	for  (i = 0; i < uCount; i++) {
		pFeedsEnum->Item(i, IID_PPV_ARGS(&pFeedSubFolder));
		
		pFeedSubFolder->Name(&lpszName);
		pFeedSubFolder->Path(&lpszPath);
		hitemChild = InsertTreeItem(m_hwndFeed, hitem, lpszName, lpszPath, NULL);
		
		pFeedSubFolder->TotalUnreadItemCount(&uUnreadItemCount);
		if (uUnreadItemCount > 0)
			SetTreeItem(hitemChild, TRUE);

		EnumFolder(hitemChild, pFeedSubFolder);

		CoTaskMemFree(lpszName);
		pFeedSubFolder->Release();
	}
	pFeedsEnum->Release();
	
	pFeedFolder->Feeds(&pFeedsEnum);
	pFeedsEnum->Count(&uCount);
	for  (i = 0; i < uCount; i++) {
		pFeedsEnum->Item(i, IID_PPV_ARGS(&pFeed));

		pFeed->Name(&lpszName);
		pFeed->Path(&lpszPath);
		pFeed->Url(&lpszUrl);

		hitemChild = InsertTreeItem(m_hwndFeed, hitem, lpszName, lpszPath, lpszUrl);

		pFeed->UnreadItemCount(&uUnreadItemCount);
		if (uUnreadItemCount > 0)
			SetTreeItem(hitemChild, TRUE);
		
		CoTaskMemFree(lpszName);
		pFeed->Release();
	}
	
	pFeedsEnum->Release();
}

IXFeedFolder::Subfoldersを呼び出せば、フォルダを列挙するためのIXFeedsEnumを取得できます。 IXFeedsEnum::Countでフォルダの数を取得したら、この数だけツリービューにアイテムを追加します。 IXFeedsEnum::Itemを呼び出せばインデックスを基にIXFeedFolderを取得できるため、 このフォルダの名前とパスをNameとPathで取得するようにします。 Nameはツリーアイテムの名前として表示され、Pathはツリーアイテムに関連付けられます。 TotalUnreadItemCountの結果が0より大きい場合は、フォルダ内に未読のフィードが存在することを意味するため、 この場合はツリーアイテムを太字で表示するようにします。 フォルダの列挙が終了したら、IXFeedFolder::Feedsを呼び出してIXFeedsEnumを取得します。 このIXFeedsEnumの場合は、ItemでIXFeedを取得できます。 フィードのツリーアイテムは、選択された際に関連するURLへアクセスすべきであるため、 IXFeed::UrlでURLを取得するようにしておきます。 UnreadItemCountの結果が0より大きい場合は、フィードが未読であることを意味するため、 ツリーアイテムを太字で表示するようにします。

ツリービューにアイテムを追加するInsertTreeItemは、次のようになっています。

HTREEITEM CFeedTree::InsertTreeItem(HWND hwndTreeView, HTREEITEM hitemParent, LPCWSTR lpszName, LPCWSTR lpszPath, LPCWSTR lpszUrl)
{
	TVINSERTSTRUCT is;
	LPFEEDDATA     lpFeedData;

	lpFeedData = (LPFEEDDATA)HeapAlloc(GetProcessHeap(), 0, sizeof(FEEDDATA));
	lpFeedData->lpszPath = lpszPath;
	lpFeedData->lpszUrl = lpszUrl;

	is.hParent      = hitemParent;
	is.item.mask    = TVIF_TEXT | TVIF_PARAM;
	is.item.lParam  = (LPARAM)lpFeedData;
	is.item.pszText = (LPWSTR)lpszName;

	lpFeedData->hitem = TreeView_InsertItem(hwndTreeView, &is);

	return lpFeedData->hitem;
}

LPFEEDDATAという型は、FEEDDATA構造体へのポインタです。 後述するイベントの処理のために、この構造体をツリーアイテムのLPARAMに関連付けておくようにします。 ツリーアイテムがフォルダであるかどうかは、 lpFeedData->lpszUrlがNULLであるかどうかで判断できます。

フィードのツリービューを完成さしたら、アイテムが選択された際の処理を実装します。

if (((LPNMHDR)lParam)->code == TVN_SELCHANGED) {
	if (m_hwndFeed == ((LPNMHDR)lParam)->hwndFrom) {
		TVITEMEX item;

		item.mask  = TVIF_HANDLE | TVIF_PARAM;
		item.hItem = ((LPNMTREEVIEW)lParam)->itemNew.hItem;
		TreeView_GetItem(m_hwndFeed, &item);

		if (item.lParam != 0) {
			LPFEEDDATA lpFeedData = (LPFEEDDATA)item.lParam;
			if (lpFeedData->lpszUrl != NULL)
				g_pWebBrowserContainer->Navigate((LPWSTR)lpFeedData->lpszUrl, FALSE);
		}
	}
}

通知コードがTVN_SELCHANGEDである場合は、アイテムが選択されたことを意味します。 maskにTVIF_PARAMを指定してTreeView_GetItemを呼び出せば、選択されたアイテムのLPARAMを取得でき、 これにはFEEDDATA構造体が関連付けられていました。 よって、LPFEEDDATAでキャストでき、アイテムがフォルダでない場合はNavigateでページにアクセスするようにします。

フィードのツリービューは、常に最新のフィードの状態を反映しているべきといえます。 たとえば、IEで何らかのフィードの名前が変更された場合は、その変更後の名前が今回のツリーアイテムにも反映されるべきです。 フィード及びフォルダに対する、名前変更や削除などはIXFeedFolderEventsとして通知されるため、 このインターフェースのメソッドを適切に実装する必要があります。

STDMETHODIMP CFeedTree::FolderAdded(LPCWSTR pszPath)
{
	AddItem(Alloc(pszPath), TRUE);

	return S_OK;
}

STDMETHODIMP CFeedTree::FeedAdded(LPCWSTR pszPath)
{
	AddItem(Alloc(pszPath), FALSE);

	return S_OK;
}

FolderAddedは新しいフォルダが追加されたことを通知し、 FeedAddedは新しいフィードが追加されたことを通知します。 これらの場合は、ツリービューに新しいアイテムを追加するべきですから、 AddItemという自作メソッドでそれを行うようにしています。 第1引数はフォルダまたはフィードのパスであり、これはツリーアイテムに関連付けることになっているため、 個別のメモリとして確保するようにしています。 第2引数は追加するアイテムがフォルダの場合はTRUEになります。

void CFeedTree::AddItem(LPCWSTR lpszPath, BOOL bFolder)
{
	HTREEITEM  hitem;
	LPFEEDDATA lpFeedData;
	LPWSTR     lpszUrl = NULL;
	LPWSTR     lpszPrentPath;
	LPWSTR     lpszName;

	GetParentPath(lpszPath, &lpszPrentPath, bFolder);

	lpFeedData = GetFeedData(lpszPrentPath, TVI_ROOT);
	if (lpFeedData == NULL)
		hitem = TVI_ROOT;
	else
		hitem = lpFeedData->hitem;

	if (!bFolder) {
		IXFeed *pFeed;

		m_pFeedsManager->GetFeed(lpszPath, IID_PPV_ARGS(&pFeed));

		pFeed->Url(&lpszUrl);
		pFeed->Release();
	}

	GetName(lpszPath, &lpszName, bFolder);

	InsertTreeItem(m_hwndFeed, hitem, lpszName, lpszPath, lpszUrl);
}

新しいツリーアイテムを追加するにあたって、どのアイテムを親にするかを決定しなければなりません。 GetParentPathは、第1引数のパスから親フォルダのパスを求める自作メソッドであり、 たとえばlpszPathが「新しいフォルダ/新しいフィード」ならば、 lpszPrentPathは「新しいフォルダ」が返ります。 GetFeedDataは、第1引数のパスを持ったツリーアイテムを検索し、それに関連付けられているFEEDDATA構造体を返します。 このhitemが親フォルダのツリーアイテムのハンドルになるため、 これを基にInsertTreeItemを呼び出せるようになります。 追加しようするアイテムがフォルダでない場合は、フィードのURLを取得するようにします。 GetNameは第1引数のパスから名前の部分を取得する自作メソッドであり、 たとえばlpszPathが「新しいフォルダ/新しいフィード」ならば、 lpszNameは「新しいフィード」が返ります。

削除に関する通知は次の2つです。

STDMETHODIMP CFeedTree::FolderDeleted(LPCWSTR pszPath)
{
	DeleteItem(pszPath);
	
	return S_OK;
}

STDMETHODIMP CFeedTree::FeedDeleted(LPCWSTR pszPath)
{
	DeleteItem(pszPath);

	return S_OK;
}

FolderDeletedはフォルダが削除された場合に呼ばれ、 FeedDeletedはフィードが削除された場合に呼ばれます。 FolderDeletedが呼ばれたということは、フォルダの中に存在するフィードも削除されたということを意味しますが、 それらのフィードに対するFeedDeletedは呼ばれないことに注意してください。 DeleteItemの実装は次のようになっています。

void CFeedTree::DeleteItem(LPCWSTR lpszPath)
{
	LPFEEDDATA lpFeedData = GetFeedData(lpszPath, TVI_ROOT);

	TreeView_DeleteItem(m_hwndFeed, lpFeedData->hitem);
}

lpszPathには削除対象のパスが格納されているため、 このパスを基に削除するアイテムを決定しなければなりません。 GetFeedDataを呼び出せばパスに関連するアイテムのハンドルを取得できるため、 これをTreeView_DeleteItemに指定します。 TreeView_DeleteItemによってTVN_DELETEITEMが送られ、 そこではアイテムに関連付けられたFEEDDATA構造体を開放します。

名前変更に関する通知は次の2つです。

STDMETHODIMP CFeedTree::FolderRenamed(LPCWSTR pszPath, LPCWSTR pszOldPath)
{
	RenameItem(pszPath, pszOldPath);

	return S_OK;
}

STDMETHODIMP CFeedTree::FeedRenamed(LPCWSTR pszPath, LPCWSTR pszOldPath)
{
	RenameItem(pszPath, pszOldPath);

	return S_OK;
}

FolderRenamedはフォルダの名前が変更された場合に呼ばれ、 FeedRenamedはフィードの名前が変更された場合に呼ばれます。 第1引数は名前を変更した後のパスであり、第2引数は変更前のパスです。 RenameItemの実装は次のようになっています。

void CFeedTree::RenameItem(LPCWSTR lpszPath, LPCWSTR lpszOldPath)
{
	TVITEMEX   item;
	LPFEEDDATA lpFeedData = GetFeedData(lpszOldPath, TVI_ROOT);
	LPWSTR     lpszName;

	CoTaskMemFree((LPVOID)lpFeedData->lpszPath);
	lpFeedData->lpszPath = Alloc(lpszPath);
	
	GetName(lpszPath, &lpszName, lpFeedData->lpszUrl == NULL);
	
	item.mask    = TVIF_TEXT | TVIF_HANDLE;
	item.hItem   = lpFeedData->hitem;
	item.pszText = lpszName;

	TreeView_SetItem(m_hwndFeed, &item);
}

名前の変更対象となっているツリーアイテムを調べるために、GetFeedDataを呼び出しています。 この段階では、アイテムに関連付けられているFEEDDATA構造体に変更前のパスが格納されているため、 第1引数にはlpszOldPathを指定するようにします。 lpFeedDataで取得したら変更前のパスを開放して新しいパスを指定し、 新しいパスの名前をGetNameで取得します。 そしてこの名前をTreeView_SetItemに指定すれば、ツリーアイテムの名前が新しいものになります。

移動に関する通知は次の2つです。

STDMETHODIMP CFeedTree::FolderMovedFrom(LPCWSTR pszPath, LPCWSTR pszOldPath)
{
	MoveItem(pszPath, pszOldPath, TRUE);
	
	return S_OK;
}

STDMETHODIMP CFeedTree::FeedMovedFrom(LPCWSTR pszPath, LPCWSTR pszOldPath)
{
	MoveItem(pszPath, pszOldPath, FALSE);

	return S_OK;
}

FolderMovedFromはフォルダを移動した場合に呼ばれ、 FeedMovedFromはフィードを移動した場合に呼ばれます。 第1引数は移動した後のパスであり、第2引数は移動する前のパスです。 第3引数はフォルダの場合はTRUEになります。 MoveItemの実装は次のようになっています。

void CFeedTree::MoveItem(LPCWSTR lpszPath, LPCWSTR lpszOldPath, BOOL bFolder)
{
	DeleteItem(lpszOldPath);

	AddItem(lpszPath, bFolder);
}

アイテムが移動したように見せかけるには、移動前のパスで識別されるアイテムを削除し、 移動後で識別されるアイテムを新しく作成すればよいことになります。

オートメーション APIについて

Windows RSS PlatformのAPIには、IXFeedFolderのようなXという文字を含むインターフェースと、 IFeedFolderのようなXという文字を含まないインターフェースが存在します。 後者のインターフェースはオートメーションに対応したAPIであり、 全てのインターフェースはIDispatchを継承するようになっています。 前者と後者のインターフェースの間に機能的な優劣はありませんが、 使いやすさとしては後者の方が劣っているといえます。 次に、後者のインターフェースを使用する例を示します。

IFeedsManager *pFeedsManager;
IDispatch     *pDispatch;
IFeedFolder   *pFeedFolder;

CoCreateInstance(CLSID_FeedsManager, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFeedsManager));

pFeedsManager->get_RootFolder(&pDispatch);
pDispatch->QueryInterface(IID_PPV_ARGS(&pFeedFolder));

オートメーションのAPIを使用する場合は、CoCreateInstanceにCLSID_FeedsManagerを指定し、 IFeedsManagerを取得します。 ルートフォルダの取得にはget_RootFolderというメソッドを使用しますが、 ここでIDispatchとして取得しなければならないのが煩わしいところです。 IDispatchからはIFeedFolderを照会できるわけですが、 IDispatchを介するため1つ手順が増えてしまうことになります。 また、文字列を取得するメソッドなどでは、文字列がBSTR型で返る特徴があります。 オートメーションのAPIは、スクリプト言語などにとっては意味のあるインターフェースになります。

オートメーションのAPIでは、イベントの取得にIFeedFolderEventsを使用しますが、 FolderAddedのようなメソッドが呼ばれることは決してありません。 全てのイベントはIDispatch::Invokeを通じて送られるようになっており、 DISPIDを基にイベントの種類を特定します。

STDMETHODIMP CFeedTree::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
	if (dispIdMember == 0x00007001)
		FolderAdded(pDispParams->rgvarg[0].bstrVal);
	else if (dispIdMember == 0x00007002)
		FolderDeleted(pDispParams->rgvarg[0].bstrVal);
	else if (dispIdMember == 0x00007003)
		FolderRenamed(pDispParams->gvarg[1].bstrVal, pDispParams->rgvarg[0].bstrVal);
	else
		;

	return S_OK;
}

上記のように、dispIdMemberを確認して対応するメンバを呼び出します。 また、引数はrgvargに逆順で格納されていることに注意してください。 DISPIDの種類を確認するには、msfeeds.dllに格納されているタイプライブラリを開きます。



戻る