LVN_ITEMCHANGED optimization in ListView control

2019-06-02 10:00发布

I'm currently using the following code to update controls in my dialog depending on the rows selected in the list-view control:

void CMyDialog::OnLvnItemchangedListTasks(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
    // TODO: Add your control notification handler code here

    ASSERT(pNMLV);
    if(pNMLV->uChanged & LVIF_STATE)
    {
        if((pNMLV->uNewState ^ pNMLV->uOldState) & LVIS_SELECTED)
        {
            //List selection changed
            updateDlgControls();
        }
    }

    *pResult = 0;
}

This method works, except that it's REALLY slow!

For instance, if my list has about 100 items in it and then I click on the first item and Shift-click on the last item (to select all items) it locks up my app for about several seconds. This won't happen if I comment out my updateDlgControls in the example above.

Is there a way to optimize the processing of LVN_ITEMCHANGED?

For instance, for 100 selected items, it's called for each and every one of them.

EDIT: Following Jonathan Potter's suggestion I changed it to this, which seems to do the job quite nicely:

void CMyDialog::OnLvnItemchangedListTasks(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
    // TODO: Add your control notification handler code here

    ASSERT(pNMLV);
    if(pNMLV->uChanged & LVIF_STATE)
    {
        if((pNMLV->uNewState ^ pNMLV->uOldState) & LVIS_SELECTED)
        {
            //Use the timer to optimize processing of multiple notifications
            //'LVN_CHANGE_OPTIMIZ_TIMER_ID' = non-zero timer ID
            //If SetTimer is called when timer was already set, it will be reset without firing it
            ::SetTimer(this->GetSafeHwnd(), LVN_CHANGE_OPTIMIZ_TIMER_ID, 1, OnLvnItemchangedListTasksTimerProc);
        }
    }

    *pResult = 0;
}

static VOID CMyDialog::OnLvnItemchangedListTasksTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
    VERIFY(::KillTimer(hwnd, idEvent));

    ASSERT(hwnd);
    ::PostMessage(hwnd, WM_APP_PROCESS_LVN_ITEMCHANGED, 0, 0);
}

ON_MESSAGE(WM_APP_PROCESS_LVN_ITEMCHANGED, OnDelayed_updateDlgControls)

LRESULT CMyDialog::OnDelayed_updateDlgControls(WPARAM, LPARAM)
{
    //List selection changed
    updateDlgControls();

#ifdef _DEBUG
    static int __n = 0;
    TRACE(CStringA(EasyFormat(L"Updated list count=%d\n", __n++)));
#endif
}

There's only one caveat that I see in this approach. One needs to make sure to check for disabled conditions additionally in processing methods for UI controls that can be disabled by the updateDlgControls() method, since having introduced a delay we can run into a problem when the processing method for a UI control can be called before the timer procedure had a chance to call updateDlgControls() method that normally disables UI controls. (This may happen, for instance, if a user repeatedly clicks shortcut keys on the keyboard.) By doing secondary checks in processing methods we make sure that disabled methods don't cause a crash.

2条回答
Emotional °昔
2楼-- · 2019-06-02 10:14

You should consider using the ListView in virtual mode instead (enabling the LVS_OWNERDATA style). It has vast improved performance over a non-virtual ListView, since you dont store the list item data in the ListView itself, it just displays data you provide from another source of your choosing. As such, it offers some additional notifications for optimizing item retrievals and updates. The LVN_ITEMCHANGED documentation states:

If a list-view control has the LVS_OWNERDATA style, and the user selects a range of items by holding down the SHIFT key and clicking the mouse, LVN_ITEMCHANGED notification codes are not sent for each selected or deselected item. Instead, you will receive a single LVN_ODSTATECHANGED notification code, indicating that a range of items has changed state.

查看更多
看我几分像从前
3楼-- · 2019-06-02 10:29

I would do this using a flag and a timer. When you get a state change message, set a flag that indicates the state has changed, and use SetTimer to kick off a short (even 1ms would probably do) timer. Since timers are low priority it won't fire while other messages, like WM_NOTIFY are in your queue. When the timer expires, kill it and then update your UI state.

(The flag is simply used so that you don't recreate the timer over and over again - once the timer has fired, use KillTimer to kill it and clear the flag ready for next time).

查看更多
登录 后发表回答