ListCtrl控件的滚动条拖动疑难杂症

发布时间:2007-7-9 11:11:00;点击率:669; 收藏本页


大家好:
    ListCtrl控件相信大家都用过,他的report风格可能许多朋友都常用,虽然很好用,但我今日确实碰到了一个问题,一时无从解决,只有在这里求助。
    我用这个report风格的listctrl显示一些内容,该listctrl中内容相当长,会出现垂直滚动条,横向也很多项,也会出现水平滚动条。
    由于实际工作的需要,我不得不每5秒钟删除该listctrl控件中的内容然后再重新写到该控件中以达到实时更新数据的效果。
    但现在的问题就产生了,比如我拖到listctrl控件的最末尾一行正在看结果,还没等看清楚,到了5秒中,由于listctrl中的内容被重新删除又重写了,所以我刚刚用鼠标拖到listctrl控件末尾没用了,我发现我又看到了listctrl的左上角去了,要看最末尾一行,我又不得不拖动鼠标,可是,又没等我看清楚又一个5秒中来了,如何是好呢。
    请大家帮我想想办法,假设我现在鼠标通过拖动listctrl滚动条拖到了最后一行看结果,我希望当5秒中来,该listctrl刷新结果的时候我仍然能够保持看到listctrl的最后一行,而不需要重新用鼠标再拖动一次listctrl的滚动条,谢谢大家了!
   





这里我没描述清楚,并不一定是拖动到最后一行,也可能是拖动到中间某行,我只是希望能够保持我目前查看的数据的在listctrl中的相对有效位置。





其实你这个问题在WEB应用程序开发的时候也是个难题,就是说为了达到实时数据的更新,页面又不全部刷新,只是部分数据更新.好在WEB里面可以用XMLHttp等技术实现,不知道MFC怎么实现啊?




不过应该是有解决的办法.
你可以计算出具体哪些位置的Iitem需要更新数据,然后更新就是了,不要整个ListCtrl都重绘.那样就不会滚动到起始位置了.
好象不是特别难实现吧,比如m_ListCtrl.SetItemText(m,n,CString)




直接调用
CListCtrl::Scroll
BOOL Scroll( CSize size );
设置到你想看的位置




谢谢legendhui,根据提示写了段代码,scroll感觉可以实现这个情况,但CSize需要.cx和.cy作为参数传递进去,能否在请问一下.cx和.cy在report风格的listctrl中用哪个函数可以正确得到,谢谢提醒。





用虚拟列表,刷新的时候取的是你缓冲的数据,不改变列表数目,也就不存在滚动问题。你只要修改缓冲中的数据,不要直接修改列表。




能够请给一段代码,实在是没有写过类似代码的经验。




你可以把滚动条拖到某个位置,鼠标不放,这时候你的程序检测鼠标是不是没放,如果没放的话,就不要更新数据,等放开后再更新。




其实鼠标没放数据也是要更新的,鼠标拖动到的位置的数据也是在更新的。
只不过是鼠标放开的时候,数据更新后,滚动到的位置就复原了。




我给一个例子:

对话框上用向导为列表控件设置控件变量:
CListCtrl m_List;
添加成员变量
int m_nTimerID; //初始化为0
CList m_lstData; //列表控件显示数据内容

初始化列表控件:
BOOL CTestListDlg::OnInitDialog()
{
CDialog::OnInitDialog();
...
m_nTimerID = 0;
m_List.InsertColumn(0, _T("顺序"), 0, 50);
m_List.InsertColumn(1, _T("时间"), 0, 150);

return TRUE;  // return TRUE  unless you set the focus to a control
}

添加一个按钮消息:
void CTestListDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
if (!m_nTimerID)
m_nTimerID = SetTimer(1, 500, NULL);
else
{
KillTimer(m_nTimerID);
m_nTimerID = 0;
}

return;
}

响应定时器消息:
void CTestListDlg::OnTimer(UINT nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
if (m_lstData.GetCount() >= 20)
{
m_lstData.RemoveHead();
m_List.Invalidate();
}
else
{
int nIndex = m_List.InsertItem(m_lstData.GetCount(), LPSTR_TEXTCALLBACK);
m_List.EnsureVisible(nIndex, true);
}
COleDateTime oTime = COleDateTime::GetCurrentTime();
m_lstData.AddTail(oTime);
SetDlgItemInt(IDC_EDIT1, m_lstData.GetCount());

CDialog::OnTimer(nIDEvent);
}

响应列表控件的LVN_GETDISPINFO消息:
void CTestListDlg::OnLvnGetdispinfoList1(NMHDR *pNMHDR, LRESULT *pResult)
{
NMLVDISPINFO *pDispInfo = reinterpret_cast(pNMHDR);
// TODO: 在此添加控件通知处理程序代码
*pResult = 0;

LV_ITEM* pItem= &(pDispInfo)->item;

int iItemIndex= pItem->iItem;

if ((pItem->mask & LVIF_TEXT) && iItemIndex < m_lstData.GetCount())
{
switch(pItem->iSubItem)
{
case 0: //fill in main text
{
CString strText;
strText.Format("%d", iItemIndex + 1);
lstrcpy(pItem->pszText, strText);
break;
}
case 1: //fill in sub item 1 text
{
COleDateTime &oTime = m_lstData.GetAt(m_lstData.FindIndex(iItemIndex));
lstrcpy(pItem->pszText, oTime.Format());
break;
}
}
}
}





注意这句:
m_List.EnsureVisible(nIndex, true);
这是为了添加新项的时候滚动到最后;由于有了条件判断,只在项目没有满的时候滚动。
项目条数在这里
if (m_lstData.GetCount() >= 20)
控制为20条,随意修改。

这是用了回调项技术,关键在于
m_List.InsertItem(m_lstData.GetCount(), LPSTR_TEXTCALLBACK);

如果需要显示的数据很多,建议使用“虚拟列表控件”技术,也即:

虚拟列表控件指具有 LVS_OWNERDATA 样式的列表视图 (ListView) 控件。该样式启用控件来支持项数达到 DWORD(默认的项数只扩展到 int)。然而,该样式的最大便利是可以使内存中一次只有一个数据项子集。这使虚拟列表视图 (ListView) 控件可以将自己借给大型信息数据库使用,而在这类数据库中已存在特定的数据访问方法。




非常感谢Mackz的帮助,以上代码我详细阅读和测试了一下,结合自己的一个实际情况,进行了思考,代码的核心意思我的理解是通过回调函数来更新ListCtrl中的数据,由于设定为显示20行,所以更新的时候滚动条的相对位置始终没变。
但现在我的情况是,首先,该ListCtrl中需要显示多少行不确定,比如第一次显示10行,第二次可能就需要显示11行(以后显示的行数肯定不会比上一次少),而且我是每过几秒钟要把该ListCtrl中的数据全部删除并重新填充进去,所以感觉该代码还不能适用于我的这种情况,不知道我理解的是否正确。
这个问题说的在白一点其实可以这样表达:
(1)我的listctrl中目前有100行,80列,超过水平,垂直屏幕,出现了水平和垂直滚动条。
(2)然后我用鼠标拖动水平和垂直滚动条,使我自己能看到第60行,40列的内容。
(3)这个时候时钟事件来了,我的listctrl中的内容被清空并被重新写入了数据,这次数据可能仍然是100行,也可能比100行多了,但肯定不会少于100行。
(4)这个时候我发现该listctrl中我能看到的又是第一行的数据了,要查看我刚刚拖动到的第60行,40列的数据,我又不得不再拖动鼠标。
为解决这个问题,我今天写了段代码,能够起到一定的效果,但我发现执行该代码后,滚动到的位置与我上一次用鼠标拖动到的位置有偏差,代码贴出来如下,请大家指导一下:

int visindex = m_list.GetTopIndex();//得到listctrl最上边可见条目索引,我拖动到60行的时候这里应该是59了。
int posx = m_list.GetScrollPos(SB_HORZ);//取得水平滚动条的位置
m_list.DeleteAllItems();//我把该listctrl中的所有数据删除了。
....
....
//以上代码我又重新向该listctrl中给了数据
RECT ys;
if(m_list.GetItemRect(visindex,&ys,LVIR_BOUNDS) != 0)//我是在想办法获取该listctrl中每行的高度值。
{
   CSize tmpsize;
   tmpsize.cx = posx;//水平滚动的位置
   tmpsize.cy = (ys.bottom-ys.top)*(visindex+1);//上次的竖直滚动到的位置就应该是这个了吧
   m_list.Scroll(tmpsize);//滚动到上次我查看数据的地方
}

代码贴完了,但没有找到哪里不对,总之滚动到的位置和上次用水表拖动到的位置有一些偏差,不知道哪个函数或者参数没用对,请大家提些改进意见。




大家好,经过进一步对我刚贴代码的测试,发现倒数第二行代码多加了个1,改为
tmpsize.cy = (ys.bottom-ys.top)*(visindex);//上次的竖直滚动到的位置就应该是这个了吧

然后我说到了:滚动到的位置和上次用水表拖动到的位置有一些偏差,我经过确认,发现我在上边贴的最后一行
m_list.Scroll(tmpsize);//滚动到上次我查看数据的地方
之后有一行:
m_list.ShowWindow(true);//防止闪烁

加这个本来是和刚开始的
m_list.ShowWindow(false);对应以防止闪烁,但发现还是有闪烁现象发生,但如果这个防止闪烁的代码放在最后,就会产生滚动到的位置和上次用水表拖动到的位置有一些偏差的问题,如果把
m_list.ShowWindow(true);//防止闪烁
放到我上边贴的代码前,则滚动不会产生偏差。
大家看我这个解决方法是否可以,如果没有补充,我将结帖,谢谢各位,尤其感谢Mackz对我的帮助。





if (m_lstData.GetCount() >= 20)
控制为20条,随意修改。

看到这句了吗?你要多少条关键在这个判断条件里解决,不管是100条还是101条,发挥想象了。

另外,之所以用回调项,就是为了避免全部删除,不删除也可以更新内容的话,何必删除呢?当然,要达到全部删除的效果,其实就是操作数据缓冲,就是代码中的m_lstData变量。说到底,你存储多少条,就显示多少条,有什么不对的呢?

你看在取文字显示的时候,
if ((pItem->mask & LVIF_TEXT) && iItemIndex < m_lstData.GetCount())
是m_lstData的内容决定了显示条目。

不过看来你还是不理解我的想法。
(摘自 http://www.net0791.com/article/14257.htm) 

你可能感兴趣的:(C++,C,Windows编程,VC++)