(Virtual) ListView items get wrong content when hovering over them, from bottom to top - c++

I use C++ Builder 2009 and have implemented a Listview virtually.
Recently I learned of an issue that (so far) only happens for 3 people out of thousands. (Obviously there must be more instances, but they remain unreported so far).
What happens is that when the user hovers over the items in the ListView control, from bottom to top, the items in the list change.
Since the control is implemented virtually the software provides the information via an event and for 'a' reason the software gets the wrong index to work with when hovering from bottom to top.
See this little video to understand the behaviour better:
https://www.youtube.com/watch?v=jNZkaBH24PY
I have been searching for a reason, on and off, for some time, hampered by the fact that I cannot repeat this problem myself, so I needed to send special builds to 1 of the 3 willing to help. And I was finally able to pinpoint it to the use of Overlay icons !
I assign an icon to the items in the list by asking Windows what icon is relevant via SHGetFileInfo
The event:
void __fastcall TFinder::ListViewData(TObject *Sender, TListItem *Item)
{
DListView->OnData(Item, DirInListView->Object(Item->Index)) ;
}
Code called by the event:
HANDLE ObjectDisplayInListView::OnData (TListItem *ListItem, DataObject *Object)
{
ListItem->Data = (void*) Object ;
ListItem->Caption = String(Object->DisplayName().c_str()) ;
if (!SystemIconsSet)
{
ListItem->ImageIndex = Object->Icon() ;
ListItem->OverlayIndex = Object->IconOverlay() ;
}
else
{
int Icon = SystemIcon(Object) ;
ListItem->ImageIndex = (Icon & 0x00FFFFFF) ;
ListItem->OverlayIndex = (Icon >> 24) - 1 ;
}
ListItem->Cut = Object->IconGreyed() ;
ListItem->StateIndex = Object->IconState() ;
return (HANDLE) ListItem ;
}
// The SystemIcon() routine:
int ObjectDisplayInListView::SystemIcon (DataObject *Object)
{
// SHFILEINFO info ; The class declared one will do !
info.hIcon = NULL ; // Just making sure
DWORD Res = SHGetFileInfoW( Object->DisplayName().c_str(),
FILE_ATTRIBUTE_NORMAL,
&info,
sizeof(SHFILEINFO) ,
SHGFI_ICON |
SHGFI_USEFILEATTRIBUTES |
SHGFI_OVERLAYINDEX
) ;
DestroyIcon(info.hIcon) ;
return info.iIcon ;
}
The subclass code (which can be removed completely and the problem still exists !!)
void __fastcall TFinder::LVNewWindowProc(Messages::TMessage &Msg)
{
if( LVOldWindowProc ) LVOldWindowProc( Msg );
if (Msg.Msg == WM_NOTIFY) // Sort arrows
{
// Not relevant for this Q but added since it is in my code
switch(((LPNMHDR)Msg.LParam)->code)
{
case HDN_ENDTRACKA:
case HDN_ENDTRACKW:
case HDN_ITEMCHANGEDA:
case HDN_ITEMCHANGEDW:
{
if(((LPNMHDR)Msg.LParam)->hwndFrom == ListView_GetHeader(ListView->Handle))
{
if (ObjInListView && dynamic_cast<FileDirObject*>(ObjInListView) && ListView->Items->Count)
{
SetSortArrow(SortingTypeToDesignColumn(UISortingType), UISortingDirection) ; // Show the arrow based on what sorting was used
}
}
}
break;
}
}
else if (Msg.Msg == CN_NOTIFY && Platform > OS_PLATF_WINXP)
{
if (reinterpret_cast<LPNMHDR>(Msg.LParam)->code == LVN_GETDISPINFOW)
{
LV_ITEM &item = reinterpret_cast<LV_DISPINFO*>(Msg.LParam)->item ;
int OverlayIndex = -1 ;
TListItem *ListItem = ListView->Items->Item[item.iItem] ;
if (ListItem) OverlayIndex = ListItem->OverlayIndex ;
if (OverlayIndex >= 0)
{
item.mask |= LVIF_STATE ;
item.state |= INDEXTOOVERLAYMASK(OverlayIndex + 1) ;
item.stateMask |= LVIS_OVERLAYMASK ;
}
}
}
}
I implemented this based on an older question, asked here as well: Can't get Windows Overlay icons to work in TListView
To be more precise, this function has been working great for me for over 10 years, but about a year ago I added SHGFI_OVERLAYINDEX as well
The issue, as explained and as can be seen in the video, disappears entirely when I simply and only remove SHGFI_OVERLAYINDEX from the function call.
For whatever reason also asking for the overlay icon, causes the weird behaviour.
Even when I completely disable the CN_NOTIFY message subclassing that goes along with this functionality (see: Can't get Windows Overlay icons to work in TListView ) but call SHGetFileInfo() with SHGFI_OVERLAYINDEX, the problem happens !
Does anybody have an idea what this could be ?
A solution could be to not ask for the overlay icon on these problematic systems, but then I need to find a way to actually detect that the problem is going to happen (or is happening) somehow.

Related

Having trouble iterating over the right children to change their color

I'm looking to make a menu where there are more than one wxStaticTexts and when one of them is clicked it turns black and the rest are/revert back to being grey (if they were clicked before, otherwise they would just stay grey)
The problem is I usurped this code which works great for doing the first part, it turns the item that was clicked black, but it doesn't turn the rest back to grey. My attempt at a solution is in the else block. I haven't attempted anything else because I'm still figuring both C++ and WxWidgets out and I still don't have a complete understanding of some concepts used in this snippet.
void MyFrame::OnMenuTxtBtnLeftClickPanel(wxMouseEvent& event) {
wxObject* obj = event.GetEventObject();
wxPanel* objPanel = ((wxPanel*)obj);
wxWindowList objChild = objPanel->GetChildren();
for (wxWindowList::iterator it = objChild.begin(); it != objChild.end(); it++) {
wxStaticText* aStaticText = dynamic_cast<wxStaticText*>(*it);
if (aStaticText) {
aStaticText->SetForegroundColour(wxColour("#000000"));
}
else {
// Doesn't do anything when compiled
// it should change the StaticTexts that weren't clicked back to grey
dynamic_cast<wxStaticText*>(*it)->SetForegroundColour(wxColour("#C8C6C6"));
}
}
This works for me:
void MyFrame::OnMenuTxtBtnLeftClickPanel(wxMouseEvent& event)
{
wxWindow* cur = wxDynamicCast(event.GetEventObject(),wxWindow);
wxColor fg = m_panel1->GetForegroundColour();
wxWindowList& children = m_panel1->GetChildren();
for ( auto it = children.begin() ; it != children.end() ; ++it )
{
wxWindow* win = *it;
if ( wxDynamicCast(win, wxStaticText) )
{
if ( win == cur )
{
win->SetForegroundColour(wxColour("#000000"));
}
else
{
win->SetForegroundColour(fg);
}
}
}
}
In this code, m_panel1 is a wxPanel that is the parent of all the static texts.
On GTK, it looks like this:
The handler was bound to each static text control in the frame constructor like this:
m_staticText1->Bind(wxEVT_LEFT_UP,&MyFrame::OnMenuTxtBtnLeftClickPanel,this);
m_staticText2->Bind(wxEVT_LEFT_UP,&MyFrame::OnMenuTxtBtnLeftClickPanel,this);
m_staticText3->Bind(wxEVT_LEFT_UP,&MyFrame::OnMenuTxtBtnLeftClickPanel,this);
m_staticText4->Bind(wxEVT_LEFT_UP,&MyFrame::OnMenuTxtBtnLeftClickPanel,this);
m_staticText1, etc. should be changed to the names you're using for the text controls.

MFC/C++ ComboBox: disable drawing of Dropdown closing & opening (UI freeze)

I've just added an Item-Filter-Feature to a CComboBox derived class called
ComboBoxFbp in an old MFC application.
BOOL CComboBoxFbp::OnEditChange()
{
CString csText;
if (m_wFbpMode & _FbpMode_UserTextFiltersList) {
GetWindowText(csText);
// This makes the DropDown "flicker"
// ShowDropDown(false);
// Just insert items that match
FilterItems(csText);
// Open DropDown (does nothing if already open)
ShowDropDown(true);
}
return FALSE; // Notification weiterleiten
}
void CComboBoxFbp::FilterItems(CString csFilterText)
{
CString csCurText;
int nCurItem;
DWORD wCurCursor;
// Text/selection/cursos restore
GetWindowText(csCurText);
nCurItem = GetCurSel();
if (nCurItem != CB_ERR && nCurItem >= 0 && nCurItem < GetCount()) {
CString csCurItemText;
GetLBText(nCurItem, csCurItemText);
if (csCurItemText == csCurText) csCurText = csCurItemText;
else nCurItem = CB_ERR;
} else {
nCurItem = CB_ERR;
}
wCurCursor = GetEditSel();
// Delete all items
ResetContent();
csFilterText.MakeLower();
// Add just the items (from the vector of all possibles) that fit
for (auto item : m_vItems)
{
CString csItemText = item.first;
csItemText.MakeLower();
if (!csFilterText.IsEmpty() && csItemText.Find(csFilterText) < 0)
continue;
const int i = AddString(item.first);
SetItemData(i, item.second);
}
// Text/selection/cursos restore
if (nCurItem != CB_ERR) SelectString(-1, csCurText);
else SetWindowText(csCurText);
SetEditSel(LOWORD(wCurCursor), HIWORD(wCurCursor));
}
So when the user types, the long list of items in the DropDown gets filtered accordingly. Everything's fine so far.
The size/height of the ListBox/DropDown doesn't change once its open. It does change accordingly when die DropDown opens. Meaning if there are only 2 items the DropDown is only 2 items high.
My issue
When the user enters a text where just one item fits the DropDown is only 1 item in height (this happens with some user workflows, i.e. user manually closes & opens the DropDown).
Now when the user now changes the text so multiple items are fitting the height stays 1 item and it looks weird as even the scrollbar doesn't look correct as it doesn't fit.
What I've tried so far
I cannot use CComboBox::SetMinVisibleItems (or the MSG behind it) as it only works in a Unicode CharacterSet (which I'm not able to change in this old application) and from WinVista onwards (app runs on WinXP).
The only other option is to close and open the DropDown so it gets redrawn correctly with the correct height (see // This makes the DropDown "flicker" in Source Code above).
Now going with option 2 I don't want the user to see the closing and opening ("flicker") of the DropDown after every key he is pressing.
To prevent this I've tried a couple of solutions I've found but none works in my case with a ComboBox-DropDown. Here's a list of methods I've put just before the ShowDropDown(false) and just after the ShowDropDown(true).
EnableWindow(false/true);
(Un)LockWindowUpdate();
SendMessage(WM_SETREDRAW, FALSE/TRUE, 0)
With all three calls I still see the DropDown closing/opening.
Do you guys have other ideas how I can prevent this flicker?
Thanks in advance
Soko
This is an XY question.
It should be easier to use the following approach to adjust the height of the ComboBox
Use GetComboBoxInfo to get the handle of the list control.
Use OnChildNotify or ON_CONTROL_REFLECT and capture CBN_DROPDOWN.
In the handler of the message resize the window as needed Use SetWindowPos and just change the size.

RTS style of drag and select system

I'm trying to make a click and drag selection system in c++ and SDL2 like the kind used in real time strategy games. You click with the mouse and drag it over what you want to select. How can I go about making it?
Edit: I know how to handle the mouse inputs. I have used a Rect structure to track the size of the selection zone. However while the zone draws correctly, the objects inside the zone don't react at all. However individually clicking on them works fine every time.
I guess my question is what is the best way to check for a group selection vs individual object selection?
You have to check what's inside the rect you are gonna drag (the test depends on the shapes that are included) and the drag those shapes aswell
I can describe how i have implemented this.
In the event handling routine, do something like the code below. I think the method names explain quite well what's happening and how i'm thinking (this is copied from my hobby-hack-RTS-engine which is based on SDL2):
case SDL_MOUSEBUTTONDOWN:
{
// Calculate index, x and y for the tile that was clicked in the map.
int iClick = m_Map.getTileIndex(event.button.x, event.button.y);
if(iClick >= 0)
{
int xClick = m_Map.getTileX(iClick);
int yClick = m_Map.getTileY(iClick);
if((int)event.button.button == 1)
{
// Unmark all MO..
for(std::list<MGMovingObject>::iterator it = m_MO.begin(); it != m_MO.end(); it++)
{
it->unMark();
}
activateFraming(event.button.x, event.button.y);
}
else
{
...
}
}
break;
}
case SDL_MOUSEBUTTONUP:
{
if((int)event.button.button == 1)
{
int endClickX = m_Map.getTileX(m_Map.getTileIndex(getFrameEndX(), getFrameEndY()));
int endClickY = m_Map.getTileY(m_Map.getTileIndex(getFrameEndX(), getFrameEndY()));
int startClickX = m_Map.getTileX(m_Map.getTileIndex(getFrameStartX(), getFrameStartY()));
int startClickY = m_Map.getTileY(m_Map.getTileIndex(getFrameStartX(), getFrameStartY()));
if(endClickX > 0 && endClickY > 0 && startClickX > 0 && startClickY > 0)
{
for(int x = std::min(startClickX, endClickX); x <= std::max(startClickX, endClickX); x++)
{
for(int y = std::min(startClickY, endClickY); y <= std::max(startClickY, endClickY); y++)
{
for(std::list<MGMovingObject>::iterator it = m_MO.begin(); it != m_MO.end(); it++)
{
if(it->getTileX() == x && it->getTileY() == y)
{
it->mark();
}
}
}
}
}
deactivateFraming();
}
else
{
...
}
break;
}
My selectable objects are stored as std::list<MGMovingObject> in m_MO.
The idea is that i save tile coordinates of the start of the selection frame and the end of the selection frame. I then iterate over the selectable objects and detect the ones inside the selection frame. I select these (mark()) and when i iterate over the objsects at a later stage, say during rendering, i can read out if they are selected (isMarked()).
If you want to steal code or ideas, here is the actual source file i copied it from: https://github.com/qmargyl/mgframework/blob/master/src/mgframework/mgframework.cpp

MFC VC++ Custom Checkbox Image

How do I get a 3-state checkbox to use a different bitmap for the Indeterminate state?
I want to change the image used by my 3-state checkboxes to use a different one; the controls are in Win98-style, and the indeterminate state of such checkboxes is difficult to distinguish from disabled checkboxes (this is presumably why they changed this for the WinXP-style controls, but I cannot use those because of other details in my project).
I'm using Visual C++ 2010, and I've defined an 8x8 bitmap in VS's Resource Editor. The bitmap's ID is IDB_INDET_CHECK.
I'm not entirely sure what the standard "technique" for something like this is; I've only really just started getting into manipulating Windows controls and MFC.
My first attempt was to create a class, CTriButton, that derives from CButton, override the DrawItem function, and try to draw it myself. I then used SubclassDlgItem to turn one of the checkboxes in my window into this class (I think?). This... sort of works? The checkbox no longer appears, and if I click on where it should be, an empty checkbox frame appears, but nothing else happens (and the debug message in my code is not being sent).
Here's the relevant code, though I'm not sure any of this is right. First, code from my window's OnInitDialog.
BOOL CAffixFilterDlg::OnInitDialog() // CAffixFilterDlg is my CDialog-derived window
{
CDialog::OnInitDialog(); // call basic version
// subclass a CButton-derived control with CTriButton
if ( CBipedHead.SubclassDlgItem(IDC_HEAD, this) ) // CBipedHead is a CTriButton member of CAffixFilterDlg, IDC_HEAD is a checkbox
SetWindowLong(CBipedHead.m_hWnd, GWL_STYLE, CBipedHead.GetStyle() | BS_OWNERDRAW); // set the ownerdraw style
else // subclassing didn't work
_ERROR("Subclassing failed."); // I do not see this error message, so SubclassDlgItem worked?
// initialization continues, but is not relevant...
UpdateWindow();
Invalidate();
return TRUE;
}
Next, the code for my custom button's DrawItem.
void CTriButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
_DMESSAGE("Drawing TriButton"); // never see this message
CDC dc;
dc.Attach(lpDrawItemStruct->hDC); //Get device context object
int nWidth = GetSystemMetrics(SM_CXMENUCHECK);
int nMargin = ( nWidth - 8 ) / 2;
CRect textRt = lpDrawItemStruct->rcItem;
textRt.right = textRt.right - nWidth - nMargin;
CString text;
GetWindowText(text);
UINT textDrawState = DST_TEXT;
if ( lpDrawItemStruct->itemState & ODS_DISABLED )
textDrawState |= DSS_DISABLED;
dc.DrawState(CPoint(textRt.left, textRt.top), textRt.Size(), text, textDrawState, TRUE, 0, (CBrush*)NULL);
CRect rt = lpDrawItemStruct->rcItem; // initial rect is for entire button
rt.left = rt.right - nWidth; // set left margin
LONG center = ( rt.bottom + rt.top ) / 2;
rt.top = center - nWidth/2;
rt.bottom = center + nWidth/2;
UINT checkDrawState = DFCS_BUTTONCHECK;
if ( lpDrawItemStruct->itemState & ODS_DISABLED )
checkDrawState |= DFCS_INACTIVE;
if ( lpDrawItemStruct->itemState & ODS_CHECKED )
checkDrawState |= DFCS_CHECKED;
else if ( GetCheck() == BST_INDETERMINATE ) {
_VMESSAGE("Indeterminate; custom draw.");
CBitmap indet_check = CBitmap();
indet_check.LoadBitmap(IDB_INDET_CHECK);
CPoint pt = CPoint(rt.left + nMargin, rt.top + nMargin);
CSize sz = CSize(8, 8);
dc.DrawState(pt, sz, &indet_check, DST_BITMAP|DSS_NORMAL);
}
dc.DrawFrameControl(rt, DFC_BUTTON, checkDrawState);
}
In OnInitDialog() you need to call InvalidateRect() after changing the window style otherwise it doesn't know it needs to be redrawn. It's also a good idea to call UpdateWindow() after changing window styles. Some information is usually cached by the common controls and won't acknowledge the change until UpdateWindow() has been called.
In DrawItem() you are responsible for rendering all states of the control. You should not call CButton::DrawItem() as it does nothing. Try something like the following:
void CTriButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CBitmap indet_check
_DMESSAGE("Drawing TriButton"); // I never see this message
int checkState = GetCheck();
if ( checkState == BST_CHECKED )
{
indet_check.LoadBitmap(IDB_INDET_CHECK);
}
else if ( checkState == BST_UNCHECKED )
{
indet_check.LoadBitmap(IDB_INDET_UNCHECKED);
}
else if ( checkState == BST_INDETERMINATE )
{
indet_check.LoadBitmap(IDB_INDET_INDETERMINATE);
}
// ... rest of your drawing code here ...
// don't forget to draw focus and push states too ;)
}
Addendum:
I can't believe I missed this first time around but your call to SubclassDlgItem is probably not having the desired effect. This call causes messages intended for the button to be processed by the controls parent window first. Because the default implementation of DrawItem in CWnd (the superclass of CDialog) does nothing the message never gets passed to the control.
Replace this with the following snippet and everything should be ok:
HWND hWndButton;
GetDlgItem(IDC_HEAD, &hWndButton);
CBipedHead.SubclassWindow(hWndButton);
Two side notes here:
It's usually not a good idea to use the same naming convention for both classes and class members. It makes for a confusing read.
I'm guessing you are always compiling and running in release mode. If you are - don't. This prevents assertions from being thrown and letting you know something is wrong.
Not the answer, but an answer: this custom CCheckBox I found more-or-less enables what I want. It doesn't, by default, allow 3 states, but I fixed that up with some of my own tweaks. I'm not 100% sure it works out of the box (I've had some issues, that don't seem to be due to my edits, but I can't be sure), but it was the solution I've used. I'm not going to call this the answer, though, in case someone can spy what was wrong with my code and wants to illuminate me.

Changing image of a menu button in a CMFCToolbar

I have a menu button inside a CMFCToolbar and I would like to replace the bitmap of the button each time a different entry is selected in the menu as each entry has its own icon.
I succeed in changing the icon using CMFCToolBarMenuButton::SetImage but it changes the icon in the menu entry too. Too bad.
alt text http://www.freeimagehosting.net/uploads/137269b0f2.jpg alt text http://www.freeimagehosting.net/uploads/879d03843a.jpg
Here is a sample of code:
if ( (pToolbar != NULL) && (idBase != 0) )
{
int ixButtonToReplace = pToolbar->CommandToIndex(idBase);
CMFCToolBarMenuButton* pBtnToReplace = dynamic_cast<CMFCToolBarMenuButton*>
(pToolbar->GetButton(ixButtonToReplace));
if ( pBtnToReplace )
{
const CObList& listCommands = pBtnToReplace->GetCommands();
POSITION pos = listCommands.GetHeadPosition();
while ( pos != NULL )
{
CMFCToolBarMenuButton* pItem = (CMFCToolBarMenuButton*) listCommands.GetNext(pos);
if ( pItem && (pItem->m_nID == idButtonToReplaceWith) )
{
pBtnToReplace->SetImage(pItem->GetImage());
}
}
}
}
Any ideas? Thank you.
It works out-of-box. The only you need is to call CMFCToolBar::AddToolBarForImageCollection so MFC could know which images to use.
Not sure what you mean by the menu button is changed too?
If another button is changed with the single setImage call to the obvious indication is they share a resource ID of some sort, the only solution would be to ensure they have different ID's (which would require making sure both resources are handled separately).
But it's a long time since I messed in MFC resource files to confirm this.