MFC自绘CListBox嵌入CLinkCtrl滚动错位及显示异常问题
MFC自绘CListBox链接控件错位及显示问题排查与解决
我在C++ MFC环境中实现了一个自绘CListBox,每个列表项包含自绘文本和CLinkCtrl控件。当前存在两个问题:
- 上下滚动时CLinkCtrl控件显示错位,但自绘文本始终正常
- 底部的一个列表项几乎总是无法显示链接控件,选中该控件时UI偶尔会更新为正确状态
相关配置与代码
1. ListBox配置
已设置为自绘模式
2. 填充ListBox代码(已取消线程,确保主线程更新UI)
//std::thread t([=]() { //myMutex.lock(); // 获取互斥锁 // 禁用控件重绘... calListBox.EnableWindow(FALSE); calListBox.SetRedraw(FALSE); // 清空所有项 for (int i = 0; i < calListBox.GetCount(); i++) { LPARAM lpData = calListBox.GetItemData(i); CalListBoxItem* item = reinterpret_cast<CalListBoxItem*>(lpData); delete item; } calListBox.ResetContent(); // 循环添加项 int index = calListBox.SendMessage(LB_ADDSTRING, 0, 0); if (index == LB_ERR) { DWORD dwError = GetLastError(); } else { CalListBoxItem* pItem = new CalListBoxItem(description, htmllink, index); calListBox.SendMessage(LB_SETITEMDATA, index, reinterpret_cast<DWORD_PTR>(pItem)); calListBox.SendMessage(LB_SETITEMHEIGHT, index, 100); } // 循环结束 calListBox.SetRedraw(TRUE); // 重新启用重绘 calListBox.EnableWindow(TRUE); //myMutex.unlock(); // 释放互斥锁 //}); //t.detach();
CalListBoxItem定义
// CalListBoxItem用于存储描述、索引和链接控件 public: ~CalListBoxItem(); CalListBoxItem(const std::string& desc, const std::string& link, int index) : m_desc(desc), m_link(link), originalIndex(index) { linkCtrl = new CCustLinkCtrl(); } public: int originalIndex; CCustLinkCtrl* linkCtrl; std::string m_desc; std::string m_link;
3. 自定义CalListBox自绘逻辑(关键部分)
AddLinkControl函数
// 添加链接控件函数 void CalListBox::AddLinkControl(CLinkCtrl* linkCtrl, CRect& rect, int nItemIndex, CString text, CString htmlLink) { if (!linkCtrl) { return; } if (::IsWindow(linkCtrl->m_hWnd)) { CString strIndex; strIndex.Format(_T(" u:%d"), nItemIndex); // 将索引转为字符串 CString strText = text + strIndex; // 拼接文本和索引 linkCtrl->SetWindowTextW(strText); linkCtrl->Invalidate(); linkCtrl->UpdateWindow(); return; // 控件已创建,直接更新 } // 创建列表框的子控件 linkCtrl->Create(_T(""), WS_CHILD | WS_VISIBLE | DT_VCENTER | SS_NOTIFY, rect, this, IDC_LINK_CTRL_LIST_ITEM + nItemIndex); CString strIndex; strIndex.Format(_T(" %d"), nItemIndex); // 将索引转为字符串 CString strText = text + strIndex; // 拼接文本和索引 linkCtrl->SetWindowTextW(strText); linkCtrl->SetItemUrl(0, htmlLink); // 必须在设置文本后调用,否则链接无效 linkCtrl->SetFont(linkFont); linkCtrl->Invalidate(); linkCtrl->UpdateWindow(); }
DrawItem函数(部分代码)
void CalListBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { if (lpDrawItemStruct->itemData == 0) { return; } CalListBoxItem* pItem = reinterpret_cast<CalListBoxItem*>(lpDrawItemStruct->itemData); if (pItem == nullptr) { return; } CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); if (pDC == nullptr) { return; } CRect rect(lpDrawItemStruct->rcItem); // 省略其他绘制逻辑... AddLinkControl(pItem->linkCtrl, leftRect2, pItem->originalIndex, L"<a>Link</a>", leftText2); }
问题分析与解决方案
问题1:滚动时链接控件错位
原因:自绘ListBox滚动时,可视区域内的项会被重新绘制,但AddLinkControl中仅更新控件文本,未同步更新控件位置。控件初始位置是第一次绘制时的坐标,滚动后项位置变化,控件位置未跟随调整,导致错位。
解决:在AddLinkControl的控件已存在分支中,添加位置更新代码:
if (::IsWindow(linkCtrl->m_hWnd)) { // 新增:同步控件位置 linkCtrl->MoveWindow(rect); // 原有文本更新逻辑 CString strIndex; strIndex.Format(_T(" u:%d"), nItemIndex); CString strText = text + strIndex; linkCtrl->SetWindowTextW(strText); linkCtrl->Invalidate(); linkCtrl->UpdateWindow(); return; }
问题2:底部项链接控件不显示,选中时偶尔恢复
原因:
- ListBox默认仅绘制可视区域内的项,底部项初始未进入可视区时
DrawItem未触发,导致控件未创建; - 控件创建时可见性未强制设置,选中项触发重绘才重新处理。
解决:
- 在
AddLinkControl中,无论控件是否已创建,都强制设置可见性:
// 控件创建完成或更新后添加 linkCtrl->ShowWindow(SW_SHOW);
- 处理ListBox滚动消息,强制同步可视项的控件位置:
void CalListBox::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { CListBox::OnVScroll(nSBCode, nPos, pScrollBar); // 刷新所有可视项的链接控件位置 int top = GetTopIndex(); int count = GetCount(); int visibleCount = GetVisibleCount(); for (int i = top; i < min(top + visibleCount, count); i++) { CalListBoxItem* pItem = reinterpret_cast<CalListBoxItem*>(GetItemData(i)); if (pItem && pItem->linkCtrl && ::IsWindow(pItem->linkCtrl->m_hWnd)) { CRect itemRect; GetItemRect(i, &itemRect); // 根据自身布局调整链接控件的相对位置,示例为偏移调整 CRect linkRect = itemRect; linkRect.DeflateRect(10, 50, 10, 10); pItem->linkCtrl->MoveWindow(linkRect); } } }
额外注意事项
- 清理内存:在清空ListBox项时,需销毁链接控件避免泄漏:
for (int i = 0; i < calListBox.GetCount(); i++) { LPARAM lpData = calListBox.GetItemData(i); CalListBoxItem* item = reinterpret_cast<CalListBoxItem*>(lpData); if (item->linkCtrl && ::IsWindow(item->linkCtrl->m_hWnd)) { item->linkCtrl->DestroyWindow(); delete item->linkCtrl; } delete item; }
- 避免在
DrawItem中重复创建控件,确保每个项的控件仅初始化一次。
内容的提问来源于stack exchange,提问作者Wayne
相关产品推荐
相关产品推荐

