[WTL/ATL]_[初级]_[使用虚拟列表视图来解决新增大量数据卡顿问题]

Posted infoworld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[WTL/ATL]_[初级]_[使用虚拟列表视图来解决新增大量数据卡顿问题]相关的知识,希望对你有一定的参考价值。

场景

  1. 在使用WTL/ATL开发界面程序时,CListViewCtrl是我们常用的表格控件。但是这个控件有个缺点,就是在连续插入大批量数据,比如10000条数据时,界面基本上是卡死的,而且内容使用量也很可观,怎么解决?

说明

  1. 我们知道WTLCListViewCtrl实际是对Win32ListView控件的薄封装,因此ListView的样式对CListViewCtrl也有效. ListView有一个样式是LVS_OWNERDATA, 它用于创建virtual listview,即虚拟列表.这种样式使控件能够处理数百万项的行数据,因为所有者(父窗口)承担了管理项数据的负担。虚拟列表视图控件本身只维护很少的项信息。除了项选择和焦点信息外,控件的所有者必须管理所有项信息。 实际就是这个样式已经不需要通过通过AddItem方法来创建每行的LVITEM对象,而是在显示的时候才去从缓存(比如一个数组里)获取每个单元格的数据,这样避免了创建过多的LVITEM对象,达到减少内存使用的目的,也因此减少了主线程创建LVITEM对象的等待时间。

  2. virtual listview需要通过自定义处理3个主要的消息通知来获取展现的数据.

    • LVN_GETDISPINFO: 获取某行某列的的数据用于行数据的显示。
    • LVN_ODFINDITEM: 判断是否能获取某行的数据,在LVM_FINDITEM消息发送时调用,对应着CListViewCtrlFindItem方法.
    • LVN_ODCACHEHINT:在调用LVN_ODFINDITEM前调用,准备某个范围的数据。很少用, 因为数据会提前准备好,可以用在LRU缓存上,比如查询某个区间的图片。
  3. 可以使用LVS_OWNERDATA样式绑定其他样式,除了LVS_SORTASCENDINGLVS_SORTDESCENDING, 因为虚拟列表视图都是LVS_AUTOARRANGE样式,它的排序由数据缓存对象控制.

  4. 为了能在列表里显示数据,必须首先调用SetItemCountSetItemCountEx方法,即发送LVM_SETITEMCOUNT消息.

  5. 以下的样式在LVS_OWNERDATA样式设置后不支持:LVM_ENABLEGROUPVIEW, LVM_GETITEMTEXT, LVM_SETTILEINFO, 和 LVM_MAPIDTOINDEX.

例子

  1. 项目例子创建了一个虚拟列表视图,每次加载新数据或插入10000条数据,速度很快,卡也支持在创建缓存对象的过程中。注意,这里的shared_ptr不是必须的.
photos_.push_back(new shared_ptr<Photo>(photo));
  1. 刷新表格会删除所有的缓存对象,删除缓存对象是需要调用delete释放,会减慢响应速度,如果想更快的速度,可以在调用listview_.SetItemCount(0);之后使用异步线程释放对象.
void CView::ReloadListView()
{
	listview_.SetItemCount(0);
	for(auto i = 0; i< photos_.size();++i)
		delete photos_[i];

	photos_.clear();
}
  1. 删除行数据,可以通过删除缓存对象和调用listview_.DeleteItem(iItem);来删除显示行。
int iItem = -1;
while((iItem = listview_.GetNextItem(iItem,LVNI_SELECTED)) != -1){
	auto ite = photos_.begin()+iItem;
	delete *ite;
	photos_.erase(ite);
	listview_.DeleteItem(iItem);
	iItem--;
}

View.h

// View.h : interface of the CView class
//
/

#pragma once

#include <utility>
#include <string>
#include <vector>
#include <memory>
#include <atlmisc.h>
#include <atlctrls.h>
#include <atlctrlx.h>
#include <GdiPlus.h>

using namespace std;

enum
{
	kMyButtonId = WM_USER+1,
	kMyButtonId2,
	kMyButtonId3,
	kMyListViewId
};

class Photo
{
public:
	wstring name;
	wstring createDate;
	wstring path;
	wstring format;
};

class CView : public CWindowImpl<CView>
{
public:
	DECLARE_WND_CLASS(NULL)

	BOOL PreTranslateMessage(MSG* pMsg);

	BEGIN_MSG_MAP_EX(CView)
		MSG_WM_CREATE(OnCreate)
		MESSAGE_HANDLER(WM_PAINT, OnPaint)
		NOTIFY_HANDLER(kMyListViewId,NM_CLICK,OnNMClickListResult)
		NOTIFY_HANDLER(kMyListViewId,LVN_GETDISPINFO,OnGetListViewData)
		NOTIFY_HANDLER(kMyListViewId,LVN_ODCACHEHINT,OnPrepareListViewData)
		NOTIFY_HANDLER(kMyListViewId,LVN_ODCACHEHINT,OnFindListViewData)
		COMMAND_RANGE_HANDLER_EX(kMyButtonId,kMyButtonId3,OnCommandIDHandlerEX)
		REFLECT_NOTIFICATIONS()
	END_MSG_MAP()

protected:
// Handler prototypes (uncomment arguments if needed):
//	LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
//	LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
//	LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)
	int OnCreate(LPCREATESTRUCT lpCreateStruct);
	LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
	void UpdateLayout();
	LRESULT OnNMClickListResult(int idCtrl,LPNMHDR pnmh,BOOL &bHandled);
	LRESULT OnGetListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled);
	LRESULT OnPrepareListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled);
	LRESULT OnFindListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled);
	void OnCommandIDHandlerEX(UINT uNotifyCode, int nID, CWindow wndCtl);

	void ReloadMockData();
	void ReloadListView();

private:
	std::wstring GetControlText(HWND hwnd,wchar_t* buf = NULL);
	CListViewCtrl listview_;
	vector<shared_ptr<Photo>*> photos_;

	CFont font_normal_;
	CFont font_bold_;

	CBrushHandle brush_white_;
	CBrushHandle brush_hollow_;
	CBrush brush_red_;

	CButton buttonReloadMockData_;
	CButton buttonReloadListView_;
	CButton buttonDeleteListViewOneRow_;
};

View.cpp

// View.cpp : implementation of the CView class
//
/

#include "stdafx.h"
#include "resource.h"
#include <utility>
#include <sstream>
#include <stdint.h>
#include <assert.h>
#include <Strsafe.h>

#include "View.h"
#include <CommCtrl.h>
#include <string>
#include <regex>

using namespace std;

BOOL CView::PreTranslateMessage(MSG* pMsg)
{
	return FALSE;
}

LRESULT CView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
	CPaintDC dc(m_hWnd);
	CMemoryDC mdc(dc,dc.m_ps.rcPaint);

	CRect rect_client;
	GetClientRect(&rect_client);
	mdc.FillSolidRect(rect_client,RGB(255,255,255));
	//TODO: Add your drawing code here

	return 0;
}

static HFONT GetFont(int pixel,bool bold,const wchar_t* font_name)
{
	LOGFONT lf; 
	memset(&lf, 0, sizeof(LOGFONT)); // zero out structure 
	lf.lfHeight = pixel; // request a 8-pixel-height font
	if(bold)
	{
		lf.lfWeight = FW_BOLD;  
	}
	lstrcpy(lf.lfFaceName, font_name); // request a face name "Arial"
	
	HFONT font = ::CreateFontIndirect(&lf);
	return font;
}


std::wstring CView::GetControlText(HWND hwnd,wchar_t* buf)
{
	auto length = ::GetWindowTextLength(hwnd);
	bool bufNull = false;
	if(!buf){
		buf = new wchar_t[length+1]();
		bufNull = true;
	}
	
	::GetWindowText(hwnd,buf,length+1);
	std::wstring str(buf);

	if(bufNull)
		delete []buf;

	return str;
}

static std::wstring GetProductBinDir()
{
	static wchar_t szbuf[MAX_PATH];  
	GetModuleFileName(NULL,szbuf,MAX_PATH);  
    PathRemoveFileSpec(szbuf);
	int length = lstrlen(szbuf);
	szbuf[length] = L'\\\\';
	szbuf[length+1] = 0;
	return std::wstring(szbuf);
}

LRESULT CView::OnGetListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled)
{
	NMLVDISPINFO* plvdi = (NMLVDISPINFO*) pnmh;
	auto iItem = plvdi->item.iItem;
	if (-1 == iItem)
		return 0;
	
	auto count = photos_.size();
	if(!count || count <= iItem)
		return 0;

	auto photo = photos_[iItem];
	if(plvdi->item.mask & LVIF_TEXT){
		switch(plvdi->item.iSubItem)
		{
		case 0:
			StringCchCopy(plvdi->item.pszText, plvdi->item.cchTextMax, to_wstring((int64_t)iItem+1).c_str());
			break;
		case 1:
			StringCchCopy(plvdi->item.pszText, plvdi->item.cchTextMax, photo->get()->name.c_str());
			break;
		case 2:
			StringCchCopy(plvdi->item.pszText, plvdi->item.cchTextMax, photo->get()->format.c_str());
			break;
		case 3:
			StringCchCopy(plvdi->item.pszText, plvdi->item.cchTextMax, photo->get()->createDate.c_str());
			break;
		}
	}
	
	return 0;
}

LRESULT CView::OnPrepareListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled)
{
	return 0;
}

LRESULT CView::OnFindListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled)
{
    LPNMLVFINDITEM  pnmfi = (LPNMLVFINDITEM)pnmh;

	auto iItem = pnmfi->iStart;
	if (-1 == iItem)
		return -1;
	
	auto count = photos_.size();
	if(!count || count <= iItem)
		return -1;
	
	return 0;
}

LRESULT CView::OnNMClickListResult(int idCtrl,LPNMHDR pnmh,BOOL &bHandled)
{
	return 0;
}

void CView::OnCommandIDHandlerEX(UINT uNotifyCode, int nID, CWindow wndCtl)
{
	switch(nID)
	{
	case kMyButtonId:
		{
			ReloadMockData();
			MessageBox(L"刷新模拟数据完成");
			break;
		}
	case kMyButtonId2:
		{
			ReloadListView();
			MessageBox(L"重新加载表格数据完成");
			break;
		}
	case kMyButtonId3:
		{
			int iItem = -1;
			while((iItem = listview_.GetNextItem(iItem,LVNI_SELECTED)) != -1){
				auto ite = photos_.begin()+iItem;
				delete *ite;
				photos_.erase(ite);
				listview_.DeleteItem(iItem);
				iItem--;
			}

			MessageBox(L"已删除");
			break;
		}
	}
}

void CView::ReloadListView()
{
	listview_.SetItemCount(0);
	for(auto i = 0; i< photos_.size();++i)
		delete photos_[i];

	photos_.clear();
}

void CView::ReloadMockData()
{
	wchar_t buf[MAX_PATH] = {0};
	LVCOLUMN co;
	memset(&co,0,sizeof(co));
	co.mask = LVCF_TEXT;
	co.pszText = buf;
	co.cchTextMax = MAX_PATH;
	listview_.GetColumn(1,&co);
	std::wstring c0Text(buf);

	listview_.GetColumn(2,&co);
	std::wstring c1Text(buf);
	
	listview_.GetColumn(3,&co);
	std::wstring c2Text(buf);

	static int index = 0;
	for(int i = 0; i< 10000;++i,++index){
		auto photo = new Photo();
		wsprintf(buf,(c0Text+L"-%d").c_str(),index);
		photo->name = buf;

		wsprintf(buf,(c1Text+L"-%d").c_str(),index);
		photo->format = buf;

		wsprintf(buf,(c2Text+L"-%d").c_str(),index);
		photo->createDate = buf;

		photos_.push_back(new shared_ptr<Photo>(photo));
	}
	listview_.SetItemCount(photos_.size());
}

int CView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	font_normal_ = ::GetFont(16,false,L"Arial");
	font_bold_ = ::GetFont(16,true,L"Arial");
	
	brush_hollow_ = AtlGetStockBrush(HOLLOW_BRUSH);
	brush_white_ = AtlGetStockBrush(WHITE_BRUSH);
	brush_red_.CreateSolidBrush(RGB(255,0,0));

	// 1.创建CListViewCtrl
	listview_.Create(m_hWnd,0,NULL,WS_CHILD | WS_TABSTOP |WS_VISIBLE
		|LVS_ALIGNLEFT|LVS_REPORT|LVS_SHOWSELALWAYS|WS_BORDER|LVS_OWNERDATA,0,kMyListViewId);
	listview_.SetExtendedListViewStyle(LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES|LVS_EX_DOUBLEBUFFER);
	listview_.SetFont(font_normal_);
	auto header = listview_.GetHeader();
	header.SetFont(font_bold_);
	listview_.SetBkColor(RGB(255,255,255));

	listview_.InsertColumn(0,L"No.",LVCFMT_LEFT,40);
	listview_.InsertColumn(1,L"Name",LVCFMT_LEFT,100);
	listview_.InsertColumn(2,L"Format",LVCFMT_LEFT,100);
	listview_.InsertColumn(3,L"Create Date",LVCFMT_LEFT,100);

	// 2.创建按钮
	buttonReloadMockData_.Create(m_hWnd,0,L"加载新数据",WS_CHILD|WS_VISIBLE,0,kMyButtonId);
	buttonReloadMockData_.SetFont(font_normal_);

	buttonReloadListView_.Create(m_hWnd,0,L"刷新表格",WS_CHILD|WS_VISIBLE,0,kMyButtonId2);
	buttonReloadListView_.SetFont(font_normal_);

	buttonDeleteListViewOneRow_.Create(m_hWnd,0,L"删除选中行",WS_CHILD|WS_VISIBLE,0,kMyButtonId3);
	buttonDeleteListViewOneRow_.SetFont(font_normal_);
	

	UpdateLayout();

	return 0;
}

void CView::UpdateLayout()
{
	CRect rect;
	GetClientRect(&rect);

	CClientDC dc(m_hWnd);
	dc.SelectFont(font_normal_以上是关于[WTL/ATL]_[初级]_[使用虚拟列表视图来解决新增大量数据卡顿问题]的主要内容,如果未能解决你的问题,请参考以下文章

[WTL/ATL]_[初级]_[微调控件CUpDownCtrl的使用]

[WTL/ATL]_[初级]_[如何设置CEdit的文本框背景色和文字颜色]

[WTL/ATL]_[初级]_[如何设置CEdit的文本框背景色和文字颜色]

[WTL/ATL]_[初级]_[如何设置CEdit的文本框背景色和文字颜色]

[WTL/ATL]_[初级]_[TreeView控件如何显示ToolTip]

[WTL/ATL]_[初级]_[关于窗口子类析构时崩溃的原因]