EternalWindows
WebBrowser コントロール / 履歴の実装

今回は、サイドバーに表示されている履歴のウインドウを実装します。 履歴のウインドウは次のような外観を持ちます。

確認したい日にちのアイテムを展開すれば、 各履歴がホスト名で区切られて表示されます。 ここからさらにアイテムを展開すれば、 そのホスト名をURLとして含む履歴が表示されます。

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

BOOL CHistoryTree::Create(HWND hwndParent)
{
	HRESULT        hr;
	LPCOLESTR      lpFilter = OLESTR("http");
	IUrlHistoryStg *pUrlHistoryStg;
	IEnumSTATURL   *pEnumSTATURL;

	hr = CoCreateInstance(CLSID_CUrlHistory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pUrlHistoryStg));
	if (FAILED(hr))
		return FALSE;

	m_hwndHistory = CreateWindowEx(0, WC_TREEVIEW, TEXT(""), WS_CHILD | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT, 0, 0, 0, 0, hwndParent, (HMENU)ID_HISTORY, NULL, NULL);

	pUrlHistoryStg->EnumUrls(&pEnumSTATURL);
	pEnumSTATURL->SetFilter(lpFilter, STATURLFLAG_ISTOPLEVEL);

	EnumHistory(pEnumSTATURL);

	pEnumSTATURL->Release();
	pUrlHistoryStg->Release();

	return TRUE;
}

IUrlHistoryStgには、履歴を管理するメソッドが含まれています。 特にEnumUrlsで取得できるIEnumSTATURLは履歴を列挙できるため、 EnumHistoryという自作メソッドに指定しています。 IEnumSTATURL::SetFilterを呼び出しているのは、列挙する履歴をhttpから始まるものに限定するためです。 これにより、fileから始まる履歴などは列挙されないことになります。

void CHistoryTree::EnumHistory(IEnumSTATURL *pEnumSTATURL)
{
	STATURL    staturl;
	SYSTEMTIME st;
	FILETIME   ft28, ft21, ft14, ft7, ft6, ft5, ft4, ft3, ft2, ft1, ft;
	HTREEITEM  hitem21, hitem14, hitem7, hitem6, hitem5, hitem4, hitem3, hitem2, hitem1, hitem;
	HTREEITEM  hitemTarget;

	hitem21 = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("3週間前"), NULL);
	hitem14 = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("2週間前"), NULL);
	hitem7 = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("1週間前"), NULL);
	hitem6 = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("6日前"), NULL);
	hitem5 = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("5日前"), NULL);
	hitem4 = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("4日前"), NULL);
	hitem3 = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("3日前"), NULL);
	hitem2 = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("2日前"), NULL);
	hitem1 = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("1日前"), NULL);
	hitem = InsertTreeItem(m_hwndHistory, TVI_ROOT, TEXT("今日"), NULL);

	GetLocalTime(&st);
	SystemTimeToFileTime(&st, &ft);
	
	GetFileTimeFromDay(&ft, &ft28, 28);
	GetFileTimeFromDay(&ft, &ft21, 21);
	GetFileTimeFromDay(&ft, &ft14, 14);
	GetFileTimeFromDay(&ft, &ft7, 7);
	GetFileTimeFromDay(&ft, &ft6, 6);
	GetFileTimeFromDay(&ft, &ft5, 5);
	GetFileTimeFromDay(&ft, &ft4, 4);
	GetFileTimeFromDay(&ft, &ft3, 3);
	GetFileTimeFromDay(&ft, &ft2, 2);
	GetFileTimeFromDay(&ft, &ft1, 1);

	while (pEnumSTATURL->Next(1, &staturl, NULL) == S_OK) {
		if (CompareFileTime(&staturl.ftLastVisited, &ft28) < 0)
			hitemTarget = NULL;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft21) < 0)
			hitemTarget = hitem21;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft14) < 0)
			hitemTarget = hitem14;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft7) < 0)
			hitemTarget = hitem7;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft6) < 0)
			hitemTarget = hitem6;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft5) < 0)
			hitemTarget = hitem5;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft4) < 0)
			hitemTarget = hitem4;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft3) < 0)
			hitemTarget = hitem3;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft2) < 0)
			hitemTarget = hitem2;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft1) < 0)
			hitemTarget = hitem1;
		else if (CompareFileTime(&staturl.ftLastVisited, &ft) < 0)
			hitemTarget = hitem;
		else
			hitemTarget = NULL;
		
		if (hitemTarget != NULL)
			InsertHistory(hitemTarget, &staturl);
	}
}

履歴のアイテムは、"1日前"や"1週間前"などの適切なアイテムの下に追加されるべきであるため、 まずは日にちに関するアイテムをツリービューに追加します。 また、どの履歴をどのアイテムの下に追加するべきかは、時間を基に判定を行わなければならないため、 それぞれの時間をft7(1週間前)やft1(1日前)などで表すようにします。 このような指定日時の時間はGetFileTimeFromDayが返すことになっており、 第1引数は現在の日にちを指定し、第3引数はその日にちから何日前を取得するかを指定します。 準備が整ったら、IEnumSTATURL::Nextで履歴の列挙を開始します。 STATURL構造体のftLastVisitedにはこの履歴のページに訪れた最後の時間が格納されており、 これと日にちを表す時間と比較します。 CompareFileTimeが0より低い値を返したということは、第1引数の時間が第2引数の時間より前ということであり、 これがft28のときに成立した場合は、履歴が4週間以上前であることを意味します。 この場合は、どのアイテムの下にも追加しないということで、hitemTargetにNULLを指定します。 ft21の条件式が成立した場合はその履歴が3週間以上前であることを意味しますが、 既に4週間以上の確認を行っているこの段階では、その履歴が3週間前であると特定できます。 よって、3週間前のアイテムを示すhitem21をhitemTargetに指定します。 この要領は他の日にちについても同一になります。 InsertHistoryを呼び出せば、hitemTargetの下に履歴のアイテムを追加できます。

履歴のアイテムは、ホストの名前を持ったフォルダの下に追加されます。 たとえば、"http://msdn.microsoft.com/en-us/library/default.aspx"という履歴は、 "msdn.microsoft.com"というフォルダの下に追加されます。 もちろん、"msdn.microsoft.com"というフォルダが作成されていない場合は、 先にフォルダを作成した後に、履歴のアイテムを追加する必要があります。 これらの点に注意して、InsertHistoryの内部を確認します。

void CHistoryTree::InsertHistory(HTREEITEM hitemTarget, LPSTATURL lpStaturl)
{
	WCHAR     szHost[256], szText[256];
	DWORD     dwSize;
	HTREEITEM hitemNext;
	TVITEM    item;
	BOOL      bHit = FALSE;

	dwSize = sizeof(szHost) / sizeof(TCHAR);
	UrlGetPartW(lpStaturl->pwcsUrl, szHost, &dwSize, URL_PART_HOSTNAME, 0);
	
	hitemNext = TreeView_GetNextItem(m_hwndHistory, hitemTarget, TVGN_CHILD);

	while (hitemNext != NULL) {
		item.mask       = TVIF_HANDLE | TVIF_TEXT;
		item.hItem      = hitemNext;
		item.pszText    = szText;
		item.cchTextMax = sizeof(szText) / sizeof(TCHAR);
		TreeView_GetItem(m_hwndHistory, &item);

		if (lstrcmpW(szText, szHost) == 0) {
			InsertTreeItem(m_hwndHistory, hitemNext, lpStaturl->pwcsTitle, lpStaturl->pwcsUrl);
			bHit = TRUE;
			break;
		}

		hitemNext = TreeView_GetNextItem(m_hwndHistory, hitemNext, TVGN_NEXT);
	}

	if (!bHit) {
		hitemTarget = InsertTreeItem(m_hwndHistory, hitemTarget, szHost, NULL);
		InsertTreeItem(m_hwndHistory, hitemTarget, lpStaturl->pwcsTitle, lpStaturl->pwcsUrl);
	}
}

まず、UrlGetPartにURL_PART_HOSTNAMEを指定してホスト名を取得します。 このホスト名は、既にフォルダが作成されているかを確認するために必要です。 hitemTargetの下位に存在するアイテムを列挙するために、TreeView_GetNextItemにTVGN_CHILDを指定します。 ここで返るアイテムは、hitemTargetの下位に存在する最初のアイテムです。 列挙したアイテムのテキストを取得したら、それがホスト名と一致するかを調べます。 これが一致する場合は、そのフォルダを親としてInsertTreeItemを呼び出して履歴のアイテムを追加します。 一致しなかった場合は、TreeView_GetNextItemにTVGN_NEXTを指定して次のアイテムを列挙します。 最終的にフォルダが存在しなかった場合は、フォルダ自体を作成するためにInsertTreeItemを呼び出します。 このとき、第3引数にはホスト名を指定し、第4引数はNULLを指定します。 戻り値として返ったハンドルをInsertTreeItemに指定することにより、 履歴のアイテムがフォルダの下に追加されるようになります。

GetFileTimeFromDayの実装は次のようになっています。

void CHistoryTree::GetFileTimeFromDay(LPFILETIME lpft, LPFILETIME lpftOut, int nDay)
{
	LONGLONG ll;
	
	ll = (LONGLONG)lpft->dwHighDateTime << 32 | lpft->dwLowDateTime;
	ll -= 60 * 60 * 24 * nDay * 10000000ULL;

	lpftOut->dwHighDateTime = (DWORD)(ll >> 32);
	lpftOut->dwLowDateTime = (DWORD)(ll & 0xFFFFFFFF);
}

現在の時間から指定時間を減算するにあたって、時間をFILETIME構造体で表していては分かりにくいため、 LONGLONG型に変換するようにします。 60 * 60というのは60秒×60分のことであり、一時間を秒単位で表しています。 これに24を掛ければ24時間を秒単位で表すことができ、さらに何日前かを表す値を掛ければ、 その日までの時間を秒単位で表すことができます。 後はこの値だけllから減算すればよいように思えますが、llは時間を100ナノ秒単位で表しているため、 10000000ULLを掛けるようにします。 計算が終了すれば、llをFILETIME構造体に変換します。

今回の履歴の読み込みは、履歴タブが初めて選択された場合に行われることに注意してください。 たとえば、履歴タブを選択した後に他のページをアクセスした場合、 そのページのURLは履歴のウインドウに追加されません。 履歴の通知を受け取り、それを動的に追加することもできそうですが、 そのためのインターフェースであると思われるIUrlHistoryStgの使い方がよく分からず、実装できていません。


戻る