WTL MFC 程序员教程,第 X 部分 - 实现拖放源






4.95/5 (37投票s)
2006 年 6 月 16 日
18分钟阅读

209040

2852
本教程介绍如何在 WTL 应用程序中使用拖放功能。
目录
引言
拖放是许多现代应用程序的功能。虽然实现放置目标相对简单,但实现拖放源则要复杂得多。MFC 提供了 COleDataObject
和 COleDropSource
类来帮助管理源必须提供的数据,但 WTL 没有此类辅助类。对于我们 WTL 用户来说,幸运的是,Raymond Chen 在 2000 年撰写了一篇 MSDN 文章(“Shell 拖放辅助对象第二部分”),其中包含了一个纯 C++ 实现的 IDataObject
,这对编写完整的 WTL 应用程序拖放源非常有帮助。
本文的示例项目是一个 CAB 文件查看器,允许您通过将文件从查看器拖放到资源管理器窗口来从中提取文件。本文还将讨论一些新的框架窗口主题,例如文件-打开处理和与 MFC 中的文档/视图框架类似的数据管理。我还将演示 WTL 的 MRU(最近最常使用)文件列表类,以及 6.0 版列表视图控件中的一些新 UI 功能。
重要提示:您需要下载并安装 Microsoft 的 CAB SDK 才能编译示例代码。SDK 的链接在 KB 文章 Q310618 中。示例项目假定 SDK 位于与源文件相同的目录中,名为“cabsdk”的目录中。
请记住,如果您在安装 WTL 或编译演示代码时遇到任何问题,请在在此处提问之前,阅读 第一部分的自述文件。
项目启动
要开始我们的 CAB 查看器应用程序,请运行 WTL AppWizard 并创建一个名为 WTLCabView 的项目。它将是一个 SDI 应用程序,因此在第一页选择 SDI Application。
在下一页,取消选中 Command Bar,并将 View Type 更改为 List View。向导将为我们的视图窗口创建一个 C++ 类,它将从 CListViewCtrl
派生。
视图窗口类如下所示:
class CWTLCabViewView : public CWindowImpl<CWTLCabViewView, CListViewCtrl> { public: DECLARE_WND_SUPERCLASS(NULL, CListViewCtrl::GetWndClassName()) // Construction CWTLCabViewView(); // Maps BEGIN_MSG_MAP(CWTLCabViewView) END_MSG_MAP() // ... };
与我们在第二部分中使用的视图窗口一样,我们可以使用 CWindowImpl
的第三个模板参数来设置默认窗口样式。
#define VIEW_STYLES \ (LVS_REPORT | LVS_SHOWSELALWAYS | \ LVS_SHAREIMAGELISTS | LVS_AUTOARRANGE ) #define VIEW_EX_STYLES (WS_EX_CLIENTEDGE) class CWTLCabViewView : public CWindowImpl<CWTLCabViewView, CListViewCtrl, CWinTraitsOR<VIEW_STYLES,VIEW_EX_STYLES> > { //... };
由于 WTL 中没有文档/视图框架,视图类将兼顾 UI 和存储 CAB 相关信息的功能。在拖放操作过程中传递的数据结构称为 CDraggedFileInfo
。
struct CDraggedFileInfo { // Data set at the beginning of a drag/drop: CString sFilename; // name of the file as stored in the CAB CString sTempFilePath; // path to the file we extract from the CAB int nListIdx; // index of this item in the list ctrl // Data set while extracting files: bool bPartialFile; // true if this file is continued in another cab CString sCabName; // name of the CAB file bool bCabMissing; // true if the file is partially in this cab and // the CAB it's continued in isn't found, meaning // the file can't be extracted CDraggedFileInfo ( const CString& s, int n ) : sFilename(s), nListIdx(n), bPartialFile(false), bCabMissing(false) { } };
视图类还具有初始化、管理文件列表以及在拖放操作开始时设置 CDraggedFileInfo
列表的方法。我不想过多的纠结于 UI 的内部工作原理,因为本文是关于拖放的,所以请查看示例项目中的 WTLCabViewView.h 以获取所有详细信息。
文件-打开处理
要查看 CAB 文件,用户会使用 File-Open 命令并选择一个 CAB 文件。向导生成的 CMainFrame
代码包含一个用于 File-Open 菜单项的处理程序。
BEGIN_MSG_MAP(CMainFrame) COMMAND_ID_HANDLER_EX(ID_FILE_OPEN, OnFileOpen) END_MSG_MAP()
OnFileOpen()
使用 CMyFileDialog
类,这是 WTL 的 CFileDialog
的增强版本,在第九部分中引入,用于显示标准的“文件打开”对话框。
void CMainFrame::OnFileOpen (
UINT uCode, int nID, HWND hwndCtrl )
{
CMyFileDialog dlg ( true, _T("cab"), 0U,
OFN_HIDEREADONLY|OFN_FILEMUSTEXIST,
IDS_OPENFILE_FILTER, *this );
if ( IDOK == dlg.DoModal(*this) )
ViewCab ( dlg.m_szFileName );
}
OnFileOpen()
调用辅助函数 ViewCab()
。
void CMainFrame::ViewCab ( LPCTSTR szCabFilename ) { if ( EnumCabContents ( szCabFilename ) ) m_sCurrentCabFilePath = szCabFilename; }
EnumCabContents()
相当复杂,它使用 CAB SDK 调用来枚举在 OnFileOpen()
中选择的文件内容并填充视图窗口。虽然 ViewCab()
目前没有做太多事情,但我们稍后会向其中添加代码以支持 MRU 列表。这是查看器显示 Windows 98 CAB 文件之一的内容时的样子:
EnumCabContents()
使用视图类中的两个方法来填充 UI:AddFile()
和 AddPartialFile()
。当文件仅部分存储在 CAB 中时,会调用 AddPartialFile()
,因为它之前在一个 CAB 文件中就开始了。在上面的屏幕截图中,列表中的第一个文件是部分文件。其余项使用 AddFile()
添加。这两个方法都为要添加的文件分配了一个数据结构,因此视图了解它显示的每个文件的所有详细信息。
如果 EnumCabContents()
返回 true,则表示枚举和 UI 设置已成功完成。如果我们正在编写一个简单的 CAB 查看器,那么就已经完成了,尽管该应用程序不会那么有趣。为了使其真正有用,我们将添加拖放支持,以便用户可以从 CAB 中提取文件。
拖放源
拖放源是一个实现两个接口的 COM 对象:IDataObject
和 IDropSource
。IDataObject
用于存储客户端在拖放操作期间想要传输的任何数据;在我们的情况下,这些数据将是 HDROP
结构,其中列出了正在从 CAB 中提取的文件。IDropSource
方法由 OLE 调用,以通知源在拖放操作期间发生的事件。
拖放源接口
实现我们放置源的 C++ 类是 CDragDropSource
。它以我介绍中提到的 MSDN 文章中的 IDataObject
实现开始。您可以在 MSDN 文章中找到有关该代码的所有详细信息,因此我在此不再赘述。然后,我们将 IDropSource
及其两个方法添加到类中。
class CDragDropSource : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CDragDropSource>, public IDataObject, public IDropSource { public: // Construction CDragDropSource(); // Maps BEGIN_COM_MAP(CDragDropSource) COM_INTERFACE_ENTRY(IDataObject) COM_INTERFACE_ENTRY(IDropSource) END_COM_MAP() // IDataObject methods not shown... // IDropSource STDMETHODIMP QueryContinueDrag ( BOOL fEscapePressed, DWORD grfKeyState ); STDMETHODIMP GiveFeedback ( DWORD dwEffect ); };
供调用者使用的辅助方法
CDragDropSource
使用几个辅助方法来封装 IDataObject
管理和拖放通信。拖放操作遵循以下模式:
- 主框架收到用户正在开始拖放操作的通知。
- 主框架调用视图窗口来构建正在拖动的文件列表。视图在
vector<CDraggedFileInfo>
中返回此信息。 - 主框架创建一个
CDragDropSource
对象,并将该向量传递给它,以便它知道要从 CAB 中提取哪些文件。 - 主框架开始拖放操作。
- 如果用户将文件拖放到合适的放置目标上,
CDragDropSource
对象将提取文件。 - 主框架更新 UI,指示任何无法提取的文件。
步骤 3-6 由辅助方法处理。初始化通过 Init()
方法完成。
bool Init(LPCTSTR szCabFilePath, vector<CDraggedFileInfo>& vec);
Init()
将数据复制到受保护的成员中,填充 HDROP
结构,并将该结构与 IDataObject
方法一起存储在数据对象中。Init()
还执行另一个重要步骤:它为每个要拖动的文件在 TEMP 目录中创建一个零字节文件。例如,如果用户从 CAB 文件拖动 buffy.txt 和 willow.txt,Init()
将在 TEMP 目录中创建两个具有相同名称的文件。这样做是为了以防拖放目标验证它从 HDROP
读取的文件名;如果文件不存在,目标可能会拒绝放置。
下一个方法是 DoDragDrop()
。
HRESULT DoDragDrop(DWORD dwOKEffects, DWORD* pdwEffect);
DoDragDrop()
在 dwOKEffects
中接受一组 DROPEFFECT_*
标志,指示源允许哪些操作。它查询必要的接口,然后调用 DoDragDrop()
API。如果拖放成功,*pdwEffect
将设置为用户想要执行的 DROPEFFECT_*
值。
最后一个方法是 GetDragResults()
。
const vector<CDraggedFileInfo>& GetDragResults();
CDragDropSource
对象维护一个 vector<CDraggedFileInfo>
,该向量在拖放操作进行时进行更新。当找到一个文件在另一个 CAB 中继续,或者无法提取时,CDraggedFileInfo
结构会根据需要进行更新。主框架调用 GetDragResults()
来获取此向量,以便它可以查找错误并相应地更新 UI。
IDropSource 方法
第一个 IDropSource
方法是 GiveFeedback()
,它通知源用户想要执行的操作(移动、复制或链接)。如果源想更改光标,也可以这样做。CDragDropSource
跟踪操作,并告诉 OLE 使用默认的拖放光标。
STDMETHODIMP CDragDropSource::GiveFeedback(DWORD dwEffect) { m_dwLastEffect = dwEffect; return DRAGDROP_S_USEDEFAULTCURSORS; }
另一个 IDropSource
方法是 QueryContinueDrag()
。OLE 在用户移动光标时调用此方法,并告知源按下了哪些鼠标按钮和键。这是大多数 QueryContinueDrag()
实现使用的样板代码:
STDMETHODIMP CDragDropSource::QueryContinueDrag ( BOOL fEscapePressed, DWORD grfKeyState ) { // If ESC was pressed, cancel the drag. // If the left button was released, do drop processing. if ( fEscapePressed ) return DRAGDROP_S_CANCEL; else if ( !(grfKeyState & MK_LBUTTON) ) { // If the last DROPEFFECT we got in GiveFeedback() // was DROPEFFECT_NONE, we abort because the allowable // effects of the source and target don't match up. if ( DROPEFFECT_NONE == m_dwLastEffect ) return DRAGDROP_S_CANCEL; // TODO: Extract files from the CAB here... return DRAGDROP_S_DROP; } else return S_OK; }
当我们看到左键已被释放时,就是从 CAB 中提取所选文件的时候。
STDMETHODIMP CDragDropSource::QueryContinueDrag ( BOOL fEscapePressed, DWORD grfKeyState ) { // If ESC was pressed, cancel the drag. // If the left button was released, do the drop. if ( fEscapePressed ) return DRAGDROP_S_CANCEL; else if ( !(grfKeyState & MK_LBUTTON) ) { // If the last DROPEFFECT we got in GiveFeedback() // was DROPEFFECT_NONE, we abort because the allowable // effects of the source and target don't match up. if ( DROPEFFECT_NONE == m_dwLastEffect ) return DRAGDROP_S_CANCEL; // If the drop was accepted, do the extracting here, // so that when we return, the files are in the temp dir // and ready for Explorer to copy. if ( ExtractFilesFromCab() ) return DRAGDROP_S_DROP; else return E_UNEXPECTED; } else return S_OK; }
CDragDropSource::ExtractFilesFromCab()
是另一个复杂的代码片段,它使用 CAB SDK 将文件提取到 TEMP 目录,覆盖我们之前创建的零字节文件。当 QueryContinueDrag()
返回 DRAGDROP_S_DROP
时,它会告诉 OLE 完成拖放操作。如果放置目标是资源管理器窗口,资源管理器会将文件从 TEMP 目录复制到放置发生的文件夹中。
从查看器拖放
现在我们已经看到了实现拖放逻辑的类,让我们看看我们的查看器应用程序如何使用该类。当主框架窗口收到 LVN_BEGINDRAG
通知消息时,它会调用视图来获取所选文件的列表,然后设置一个 CDragDropSource
对象。
LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr) { vector<CDraggedFileInfo> vec; CComObjectStack<CDragDropSource> dropsrc; DWORD dwEffect = 0; HRESULT hr; // Get a list of the files being dragged (minus files // that we can't extract from the current CAB). if ( !m_view.GetDraggedFileInfo(vec) ) return 0; // do nothing // Init the drag/drop data object. if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) ) return 0; // do nothing // Start the drag/drop! hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect); return 0; }
第一个调用是视图的 GetDraggedFileInfo()
方法来获取所选文件的列表。此方法返回一个 vector<CDraggedFileInfo>
,我们使用它来初始化 CDragDropSource
对象。如果所选文件都是我们知道无法提取的文件(例如,部分存储在另一个 CAB 文件中的文件),GetDraggedFileInfo()
可能会失败。如果发生这种情况,OnListBeginDrag()
将静默失败并返回而不执行任何操作。最后,我们调用 DoDragDrop()
来开始操作,并让 CDragDropSource
处理其余部分。
上面列表中的步骤 6 提到了在拖放完成后更新 UI。CAB 末尾的一个文件可能仅部分存储在该 CAB 中,其余部分在后续 CAB 中。(在 Windows 9x 设置文件中,CAB 文件的大小适合软盘,这种情况很常见。)当我们尝试提取 such a file 时,CAB SDK 会告诉我们包含该文件剩余部分 CAB 的名称。它还会查找与初始 CAB 相同目录中的 CAB,并在后续 CAB 存在时提取文件的其余部分。
由于我们想在视图窗口中指示部分文件,因此 OnListBeginDrag()
检查拖放结果,以确定是否找到任何部分文件。
LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr) { //... // Start the drag/drop! hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect); if ( FAILED(hr) ) ATLTRACE("DoDragDrop() failed, error: 0x%08X\n", hr); else { // If we found any files continued into other CABs, update the UI. const vector<CDraggedFileInfo>& vecResults = dropsrc.GetDragResults(); vector<CDraggedFileInfo>::const_iterator it; for ( it = vecResults.begin(); it != vecResults.end(); it++ ) { if ( it->bPartialFile ) m_view.UpdateContinuedFile ( *it ); } } return 0; }
我们调用 GetDragResults()
来获取一个更新的 vector<CDraggedFileInfo>
,它反映了拖放操作的结果。如果某个结构体的 bPartialFile
成员为 true
,则表示该文件仅部分包含在 CAB 中。我们调用视图方法 UpdateContinuedFile()
,并将信息结构传递给它,以便它可以相应地更新文件列表视图项。以下是应用程序在找到后续 CAB 时如何指示部分文件:
如果找不到后续 CAB,应用程序会通过设置 LVIS_CUT
样式来指示文件无法提取,因此图标会显示为灰色。
为了安全起见,应用程序会将提取的文件保留在 TEMP 目录中,而不是在拖放完成后立即清理它们。当 CDragDropSource::Init()
创建零字节的临时文件时,它还会将每个文件名添加到全局向量 g_vecsTempFiles
中。临时文件在主框架窗口关闭时删除。
添加 MRU 列表
我们接下来要看的文档/视图式功能是最近最常使用 (MRU) 文件列表。WTL 的 MRU 实现是模板类 CRecentDocumentListBase
。如果您不需要覆盖任何默认的 MRU 行为(通常默认行为已足够),则可以使用派生类 CRecentDocumentList
。
CRecentDocumentListBase
模板具有以下参数:
template <class T, int t_cchItemLen = MAX_PATH, int t_nFirstID = ID_FILE_MRU_FIRST, int t_nLastID = ID_FILE_MRU_LAST> CRecentDocumentListBase
T
- 派生类名称,它专门化
CRecentDocumentListBase
。 t_cchItemLen
- 要存储在 MRU 项目中的字符串的
TCHAR
长度。此值必须至少为 6。 t_nFirstID
- 要用于 MRU 项目的 ID 范围内的最低 ID。
t_nLastID
- 要用于 MRU 项目的 ID 范围内的最高 ID。此值必须大于
t_nFirstID
。
要向我们的应用程序添加 MRU 功能,我们需要执行几个步骤:
- 在您希望 MRU 项目出现的位置插入一个 ID 为
ID_FILE_MRU_FIRST
的菜单项。如果 MRU 列表为空,将显示此项的文本。 - 添加一个 ID 为
ATL_IDS_MRU_FILE
的字符串表条目。此字符串用于 MRU 项目被选中时的飞入帮助。如果您使用 WTL AppWizard,则此字符串已为您创建。 - 向
CMainFrame
添加一个CRecentDocumentList
对象。 - 在
CMainFrame::Create()
中初始化该对象。 - 处理命令 ID 在
ID_FILE_MRU_FIRST
和ID_FILE_MRU_LAST
之间的WM_COMMAND
消息。 - 打开 CAB 文件时更新 MRU 列表。
- 应用程序关闭时保存 MRU 列表。
请记住,如果您不能使用 ID_FILE_MRU_FIRST
和 ID_FILE_MRU_LAST
(如果它们不适合您的应用程序),您可以通过创建 CRecentDocumentListBase
的新专用类来更改 ID 范围。
设置 MRU 对象
第一步是添加一个菜单项,指示 MRU 项目将出现在何处。通常放在“*文件*”菜单中,这就是我们在应用程序中要使用的。这是我们的占位符菜单项:
AppWizard 已将字符串 ATL_IDS_MRU_FILE
添加到我们的字符串表中;我们将更改它以显示“打开此 CAB 文件”。接下来,我们在 CMainFrame
中添加一个名为 m_mru
的 CRecentDocumentList
成员变量,并在 OnCreate()
中进行初始化。
#define APP_SETTINGS_KEY \ _T("software\\Mike's Classy Software\\WTLCabView"); LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs ) { HWND hWndToolBar = CreateSimpleToolBarCtrl(...); CreateSimpleReBar ( ATL_SIMPLE_REBAR_NOBORDER_STYLE ); AddSimpleReBarBand ( hWndToolBar ); CreateSimpleStatusBar(); m_hWndClient = m_view.Create ( m_hWnd, rcDefault ); m_view.Init(); // Init MRU list CMenuHandle mainMenu = GetMenu(); CMenuHandle fileMenu = mainMenu.GetSubMenu(0); m_mru.SetMaxEntries(9); m_mru.SetMenuHandle ( fileMenu ); m_mru.ReadFromRegistry ( APP_SETTINGS_KEY ); // ... }
前两个方法设置我们想要的 MRU 中的项目数(默认值为 16),以及包含占位符项的菜单句柄。ReadFromRegistry()
从注册表中读取 MRU 列表。它使用我们传递给它的键名,并在其下创建一个新键来存储列表。在我们的例子中,键是 HKCU\Software\Mike's Classy Software\WTLCabView\Recent Document List
。
加载文件列表后,ReadFromRegistry()
调用另一个 CRecentDocumentList
方法 UpdateMenu()
,该方法找到占位符菜单项并用实际的 MRU 项目替换它。
处理 MRU 命令并更新列表
当用户选择一个 MRU 项目时,主框架会收到一个 WM_COMMAND
消息,其命令 ID 等于菜单项 ID。我们可以使用消息映射中的一个宏来处理这些命令:
BEGIN_MSG_MAP(CMainFrame) COMMAND_RANGE_HANDLER_EX( ID_FILE_MRU_FIRST, ID_FILE_MRU_LAST, OnMRUMenuItem) END_MSG_MAP()
消息处理程序从 MRU 对象获取项目的完整路径,然后调用 ViewCab()
以便应用程序显示该文件的内容。
void CMainFrame::OnMRUMenuItem ( UINT uCode, int nID, HWND hwndCtrl ) { CString sFile; if ( m_mru.GetFromList ( nID, sFile ) ) ViewCab ( sFile, nID ); }
如前所述,我们将扩展 ViewCab()
以使其了解 MRU 对象,并根据需要更新文件列表。新的原型是:
void ViewCab ( LPCTSTR szCabFilename, int nMRUID = 0 );
如果 nMRUID
为 0,则 ViewCab()
是从 OnFileOpen()
调用。否则,用户选择了一个 MRU 菜单项,nMRUID
是 OnMRUMenuItem()
收到的命令 ID。这是更新后的代码:
void CMainFrame::ViewCab ( LPCTSTR szCabFilename, int nMRUID ) { if ( EnumCabContents ( szCabFilename ) ) { m_sCurrentCabFilePath = szCabFilename; // If this CAB file was already in the MRU list, // move it to the top of the list. Otherwise, // add it to the list. if ( 0 == nMRUID ) m_mru.AddToList ( szCabFilename ); else m_mru.MoveToTop ( nMRUID ); } else { // We couldn't read the contents of this CAB file, // so remove it from the MRU list if it was in there. if ( 0 != nMRUID ) m_mru.RemoveFromList ( nMRUID ); } }
当 EnumCabContents()
成功时,我们会根据 CAB 文件选择的方式以不同的方式更新 MRU。如果它是通过 File-Open 选择的,我们调用 AddToList()
将文件名添加到 MRU 列表。如果它是通过 MRU 菜单项选择的,我们将其移动到列表顶部,使用 MoveToTop()
。如果 EnumCabContents()
失败,我们使用 RemoveFromList()
将文件名从 MRU 列表中删除。所有这些方法都会在内部调用 UpdateMenu()
,因此“*文件*”菜单会自动更新。
保存 MRU 列表
当应用程序关闭时,我们将 MRU 列表保存回注册表。这很简单,只需一行代码:
m_mru.WriteToRegistry ( APP_SETTINGS_KEY );
这行代码应放在 CMainFrame
处理 WM_DESTROY
和 WM_ENDSESSION
消息的处理程序中。
其他 UI 改进
透明拖放图像
Windows 2000 及更高版本有一个内置的 COM 对象,称为拖放帮助程序,其目的是在拖放操作期间提供一个漂亮的透明拖放图像。拖放源通过 IDragSourceHelper
接口使用此对象。以下是我们在 OnListBeginDrag()
中添加的额外代码(以粗体显示),用于使用帮助程序对象:
LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr) { NMLISTVIEW* pnmlv = (NMLISTVIEW*) phdr; CComPtr<IDragSourceHelper> pdsh; vector<CDraggedFileInfo> vec; CComObjectStack<CDragDropSource> dropsrc; DWORD dwEffect = 0; HRESULT hr; if ( !m_view.GetDraggedFileInfo(vec) ) return 0; // do nothing if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) ) return 0; // do nothing // Create and init a drag source helper object // that will do the fancy drag image when the user drags // into Explorer (or another target that supports the // drag/drop helper interface). hr = pdsh.CoCreateInstance ( CLSID_DragDropHelper ); if ( SUCCEEDED(hr) ) { CComQIPtr<IDataObject> pdo; if ( pdo = dropsrc.GetUnknown() ) pdsh->InitializeFromWindow ( m_view, &pnmlv->ptAction, pdo ); } // Start the drag/drop! hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect); // ... }
我们首先创建拖放帮助程序 COM 对象。如果成功,我们会调用 InitializeFromWindow()
并传递三个参数:拖放源窗口的 HWND
,光标位置,以及我们 CDragDropSource
对象上的 IDataObject
接口。拖放帮助程序使用此接口来存储其自己的数据,如果放置目标也使用帮助程序对象,则该数据用于生成拖放图像。
为了使 InitializeFromWindow()
工作,拖放源窗口需要处理 DI_GETDRAGIMAGE
消息,并响应该消息创建一个用作拖放图像的位图。对我们来说幸运的是,列表视图控件支持此功能,因此我们只需少量工作即可获得拖放图像。以下是拖放图像的外观:
如果我们使用其他窗口作为我们的视图类,并且该窗口不支持 DI_GETDRAGIMAGE
,我们将自己创建拖放图像并调用 InitializeFromBitmap()
来将图像存储在拖放帮助程序对象中。
透明选择矩形
从 Windows XP 开始,列表视图控件可以显示一个透明的选择范围框。默认情况下,此功能处于关闭状态,但可以通过在控件上设置 LVS_EX_DOUBLEBUFFER
样式来启用。我们的应用程序在 CWTLCabViewView::Init()
的视图窗口初始化过程中执行此操作。结果如下:
如果您没有看到透明范围框,请检查您的系统属性并确保在那里启用了该功能。
指示排序的列
在 Windows XP 及更高版本上,报表模式下的列表视图控件可以有一个选定的列,它显示不同的背景色。此功能通常用于指示列正在排序,这就是我们的 CAB 查看器所做的。标题控件还有两个新的格式化样式,使标题在列中显示向上或向下的箭头。这通常用于显示排序方向。
视图类在 LVN_COLUMNCLICK
处理程序中处理排序。用于显示已排序列的代码以粗体突出显示:
LRESULT CWTLCabViewView::OnColumnClick ( NMHDR* phdr ) { int nCol = ((NMLISTVIEW*) phdr)->iSubItem; // If the user clicked the column that is already sorted, // reverse the sort direction. Otherwise, go back to // ascending order. if ( nCol == m_nSortedCol ) m_bSortAscending = !m_bSortAscending; else m_bSortAscending = true; if ( g_bXPOrLater ) { HDITEM hdi = { HDI_FORMAT }; CHeaderCtrl wndHdr = GetHeader(); // Remove the sort arrow indicator from the // previously-sorted column. if ( -1 != m_nSortedCol ) { wndHdr.GetItem ( m_nSortedCol, &hdi ); hdi.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP); wndHdr.SetItem ( m_nSortedCol, &hdi ); } // Add the sort arrow to the new sorted column. hdi.mask = HDI_FORMAT; wndHdr.GetItem ( nCol, &hdi ); hdi.fmt |= m_bSortAscending ? HDF_SORTUP : HDF_SORTDOWN; wndHdr.SetItem ( nCol, &hdi ); } // Store the column being sorted, and do the sort m_nSortedCol = nCol; SortItems ( SortCallback, (LPARAM)(DWORD_PTR) this ); // Indicate the sorted column. if ( g_bXPOrLater ) SetSelectedColumn ( nCol ); return 0; }
突出显示的第一个代码段从先前排序的列中移除排序箭头。如果没有已排序的列,则跳过此部分。然后,将箭头添加到用户刚刚单击的列。如果排序是升序,箭头向上;如果排序是降序,箭头向下。排序完成后,我们调用 SetSelectedColumn()
,这是 LVM_SETSELECTEDCOLUMN
消息的包装器,以将选定列设置为我们刚刚排序的列。
以下是按大小对文件排序时列表控件的外观:
使用瓦片视图模式
在 Windows XP 及更高版本上,列表视图控件有一个名为“*瓦片视图模式*”的新样式。作为视图窗口初始化的一部分,如果应用程序正在 XP 或更高版本上运行,它会使用 SetView()
(LVM_SETVIEW
消息的包装器)将列表视图模式设置为瓦片模式。然后,它会填充一个 LVTILEVIEWINFO
结构来设置控制瓦片绘制的一些属性。cLines
成员设置为 2,表示将在每个瓦片旁边显示 2 行附加文本。dwFlags
成员设置为 LVTVIF_AUTOSIZE
,这使得控件在控件本身大小调整时会调整瓦片区域的大小。
void CWTLCabViewView::Init() { // ... // On XP, set some additional properties of the list ctrl. if ( g_bXPOrLater ) { // Turning on LVS_EX_DOUBLEBUFFER also enables the // transparent selection marquee. SetExtendedListViewStyle ( LVS_EX_DOUBLEBUFFER, LVS_EX_DOUBLEBUFFER ); // Default to tile view. SetView ( LV_VIEW_TILE ); // Each tile will have 2 additional lines (3 lines total). LVTILEVIEWINFO lvtvi = { sizeof(LVTILEVIEWINFO), LVTVIM_COLUMNS }; lvtvi.cLines = 2; lvtvi.dwFlags = LVTVIF_AUTOSIZE; SetTileViewInfo ( &lvtvi ); } }
设置瓦片视图图像列表
对于瓦片视图模式,我们将使用超大系统图像列表(在默认显示设置下有 48x48 的图标)。我们使用 SHGetImageList()
API 获取此图像列表。SHGetImageList()
与 SHGetFileInfo()
不同,因为它返回图像列表对象的 COM 接口。视图窗口有两个用于管理此图像列表的成员变量:
CImageList m_imlTiles; // the image list handle CComPtr<IImageList> m_TileIml; // COM interface on the image list
视图窗口在 InitImageLists()
中获取超大图像列表。
HRESULT (WINAPI* pfnGetImageList)(int, REFIID, void**); HMODULE hmod = GetModuleHandle ( _T("shell32") ); (FARPROC&) pfnGetImageList = GetProcAddress(hmod, "SHGetImageList"); hr = pfnGetImageList ( SHIL_EXTRALARGE, IID_IImageList, (void**) &m_TileIml ); if ( SUCCEEDED(hr) ) { // HIMAGELIST and IImageList* are interchangeable, // so this cast is OK. m_imlTiles = (HIMAGELIST)(IImageList*) m_TileIml; }
如果 SHGetImageList()
成功,我们可以将 IImageList*
接口转换为 HIMAGELIST
,并像使用任何其他图像列表一样使用它。
使用瓦片视图图像列表
由于列表控件没有用于瓦片视图模式的独立图像列表,因此当用户选择大图标或瓦片视图模式时,我们需要在运行时更改图像列表。视图类有一个 SetViewMode()
方法,用于处理图像列表和视图样式的更改。
void CWTLCabViewView::SetViewMode ( int nMode ) { if ( g_bXPOrLater ) { if ( LV_VIEW_TILE == nMode ) SetImageList ( m_imlTiles, LVSIL_NORMAL ); else SetImageList ( m_imlLarge, LVSIL_NORMAL ); SetView ( nMode ); } else { // omitted - no image list changing necessary on // pre-XP, just modify window styles } }
如果控件将进入瓦片视图模式,我们将控件的图像列表设置为 48x48 的列表,否则将其设置为 32x32 的列表。
设置附加文本行
在初始化期间,我们将瓦片设置为显示两行附加文本。第一行始终是项目文本,就像在大图标和小图标模式下一样。在两行附加文本中显示的文本取自子项,与报表模式下的列类似。我们可以单独设置每个瓦片的子项。以下是视图如何在 AddFile()
中设置文本:
// Add a new list item. int nIdx; nIdx = InsertItem ( GetItemCount(), szFilename, info.iIcon ); SetItemText ( nIdx, 1, info.szTypeName ); SetItemText ( nIdx, 2, szSize ); SetItemText ( nIdx, 3, sDateTime ); SetItemText ( nIdx, 4, sAttrs ); // On XP+, set up the additional tile view text for the item. if ( g_bXPOrLater ) { UINT aCols[] = { 1, 2 }; LVTILEINFO lvti = { sizeof(LVTILEINFO), nIdx, countof(aCols), aCols }; SetTileInfo ( &lvti ); }
aCols
数组包含应显示的子项的文本,在此我们显示子项 1(文件类型)和 2(文件大小)。以下是瓦片视图模式下的查看器外观:
请注意,在报表模式下对列进行排序后,附加行会发生变化。当通过 LVM_SETSELECTEDCOLUMN
设置选定列时,该子项的文本始终显示在第一个,覆盖我们在 LVTILEINFO
结构中传递的子项。
版权和许可
本文是受版权保护的材料,©2006 Michael Dunn。我知道这无法阻止人们在互联网上传播它,但我还是必须说。如果您有兴趣翻译本文,请给我发电子邮件告知我。我不认为会拒绝任何人翻译的许可,我只是想知道翻译情况,以便在此处发布链接。
伴随本文的演示代码已发布到公共领域。我以这种方式发布代码是为了让所有人受益。(我不将本文本身发布到公共领域,因为仅在 CodeProject 上提供本文有助于提高我的知名度和 CodeProject 网站。)如果您在自己的应用程序中使用演示代码,请通过电子邮件告知我(仅为了满足我对人们是否从我的代码中受益的好奇心),但这并非必需。在您自己的源代码中注明出处也受赞赏,但非必需。
修订历史
2006 年 6 月 16 日:文章首次发布。
系列导航:« 第九部分 (GDI 包装器)