Newsgroup: it.comp.lang.c++
Titolo: ToolTip per una ListBox

> 
http://www.geocities.com/sunsetstrip/stage/7787
 <<
<<<<<<<<<<<< _
Metal...@iol.it
 >>>>>>>>>>>>>>
(remove asterisk to reply)

ho provato ad usare un  CToolTipCtrl ma non funziona:
Eh, non sono le cose piu` facili da usare, a meno che la "parent window"
non erediti da CFrameWnd.  Per fare i tooltip in un dialogo, la cosa piu`
semplice, secondo me, e` scordarsi CToolTipCtrl e farlo "a mano", come
segue.
1 -- aggiungi:
    EnableToolTips(TRUE);
(nell'OnInitDialog, o dove preferisci);
2 -- aggiungi al tuo dialogo una member function dichiarata
       piu` o meno come segue:
BOOL OnTooltipText(UINT, NMHDR* pNmHdr, LRESULT*);
(non do` nome al 1o e 3o parametro perche` da ignorare);
3 -- aggiungi alla message map del tuo dialogo la macro:
ON_NOTIFY_EX( TTN_NEEDTEXT, 0, OnTooltipText )
(Nota che l'identificatore di tool e` 0 -- come, per usare bene
i ToolTipCtrl, avresti dovuto fare anche tu nell'AddTool,
rinunciando al RECT -- ma e` un po' lungo da spiegare...!!!)
4 -- implementa la OnTooltipText, ad esempio:
BOOL CMidiaDlg::OnTooltipText(UINT, NMHDR* pNmHdr, LRESULT*)
{
    TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNmHdr;
    wsprintf(pTTT->szText,    // nb: max 80 caratteri!
        "NH: Id=%d, TT: Fl=%x",
        pNmHdr->idFrom, pTTT->uFlags);
    return TRUE;
}
Vedrai cosi` che, in pratica, il cosiddetto idFrom e` sempre una
HWND (come confermato dal campo flags pari a 1), quindi su
quello puoi testare se riguardi o meno un tuo control sul quale
vuoi in effetti dare un tooltip, e quale (puoi ottenere l'effettivo ID,
se preferisci testare quello, con GetDlgCtrlID, naturalmente).
Se non intendi offrire un tooltip su di un dato control, dovrai
allora tornare un risultato di FALSE.
Se devi mostrare piu` di 80 caratteri, lascia stare szText e setta
invece il puntatore lpszText, sempre della struttura cui punta pTTT,
all'indirizzo della stringa da mostrare -- assicurati che la stringa in
questione resti viva abbastanza a lungo (buffer statico, o ottenuta
con malloc o new e liberata poi solo piu` tardi).  Puoi anche usare
una risorsa-stringa -- vedi l'help in linea, se ti occorre.
I CTooltipCtrl aiutano un minimo per alcune di queste cose, ma
in realta` convengono solo in casi un po' piu` complicati -- quelli
in cui, volendo, hai anche l'alternativa dell'override della funzione
membro OnToolHitTest.  Per un uso ragionevolmente semplice,
come mi pare sia quello che serve a te, io penso che il piu` grezzo
approccio che qui ho tratteggiato possa esserti piu` utile.
Alex

allora tornare un risultato di FALSE.
non riesco a fare questo controllo.
Dal debugger ho scoperto che il campo pTTT->uflags non vale 1 ma 3.
questo vuol dire che il campo pNmHdr->idFrom non è un HWND, giusto? e cos'è
allora? ho provato a fare questo controllo(m_BatchList è il CListBox che
voglio "ToolTippare" ;-)):
BOOL CMidiaDlg::OnTooltipText(UINT, NMHDR* pNmHdr, LRESULT*)
{
    TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNmHdr;
    if (pNmHdr->idFrom==m_BatchList.GetDlgCtrlID()){
            wsprintf(pTTT->szText,    // nb: max 80 caratteri!
                "NH: Id=%d, TT: Fl=%x",
                pTTTStruct->idFrom, pTTT->uFlags);
           return TRUE;
     }
     else return FALSE;
};
però non funziona: il numero pTTTStruct->idFrom quando il mouse resta sulla
listbox è diverso da quello ritornato da m_BatchList.GetDlgCtrlID().
(c'è anche un warning perchè confronto una var di tipo UINT con una di tipo
int).
E comunque se il campo idFrom contiene un HWND, a cosa serve il campo
hwndFrom della struttura NMHDR che è invece un HWND? (ho fatto delle prove e
questo hwndFrom ritorna sempre lo stesso valore, anche se il mouse si ferma
su controlli diversi).
E poi non ho capito bene se è possibile fare quello che voglio io in questo
modo:
io vorrei che il ToolTip si andasse a posizionare sopra alla stringa
visualizzata a metà nella listbox in modo da farla leggere tutta (come
succede nel DevStudio quando il nome di una funzione non ci sta per intero
nella finestra del workspace).
Così come mi hai suggerito tu, il tooltip viene invece visualizzato sotto la
finestra della ListBox.


finestra della ListBox.
Visto che hai l'HWND della finestra (quella del tooltip) che vuoi
piazzare a certe coordinate... semplicemente, fallo -- chiamando
::SetWindowPos.  Ti tocca calcolare le coordinate (di schermo).
Ecco un esempio piu` articolato di gestione del need-text che puo`
forse chiarirti meglio lo schema:
BOOL CMidiaDlg::OnTooltipText(UINT, NMHDR* pNmHdr, LRESULT*)
{
    TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNmHdr;
    static char buf[120];
    char bub[60];
    // per verificare di che tipo di window e` una certa handle:
    GetClassName(pNmHdr->hwndFrom, bub, 60);
    wsprintf(buf,
        "NH: Hw=%x(%s), Id=%d(%x), Co=%d; TT: Fl=%x",
        pNmHdr->hwndFrom, bub,
        pNmHdr->idFrom,
            (pTTT->uFlags&TTF_IDISHWND)?
            ::GetDlgCtrlID((HWND)pNmHdr->idFrom):0,
        pNmHdr->code,
        pTTT->uFlags);
    pTTT->lpszText = buf;
    ::SetWindowPos(pNmHdr->hwndFrom, HWND_TOP, 7, 7, 0, 0,
        SWP_NOSIZE|SWP_NOZORDER);
    return TRUE;
}
Questo piazza il tooltip sempre nell'angolo in alto a sinistra
dello schermo -- coordinate dell'origine, (7,7).  A te il piacere
di calcolare le esatte coordinate di schermo a cui vuoi invece
che appaia se e` sulla tua listbox...:-).
Alex


ho provato ad utilizzare la funzione che mi hai suggerito, e funziona
benissimo per i controlli come pulsanti e anche per la listbox, ma tutta
intera.
Nel senso che la parte di MFC che gestisce l'attivazione del tooltip
considera la mia listbox come un'oggetto unico e, fintanto che il mouse si
muove sopra alla listbox, il tooltip che viene visualizzato è sempre lo
stesso.
Invece il tooltip dovrebbe poter cambiare ogni volta che il mouse si muove
da un item all'altro all'interno della listbox.
E poi ho notato che il tooltip sparisce appena il mouse ci va sopra. Nel mio
caso dovrebbe succedere il contrario, e cioè: se il mouse va sopra una
stringa della listbox, questo ipotetico tooltip si dovrebbe sovrapporre alla
stringa fintanto che il mouse non se ne va da qualche altra parte.
Comincio a pensare che quello che mi serve non sia proprio un tooltip.....
Per questo stavo provando a derivare una classe da CListBox che
implementasse da sola questo meccanismo intercettando i messaggi
WM_MOUSEMOVE:
class CToolTippedListBox : public CListBox
Purtroppo ho qualche difficoltà....
Mi sono venute due idee possibili:
--usare un CToolTipCtrl come variabile membro di questa classe (magari
questo oggettino potrebbe preoccuparsi di disegnare la window del tooltip)
ho anche creato la funzione in risposta alla TTN_NEEDTEXT ma non ho ottenuto
buoni risultati.
--crearmi da solo una CWnd (variabile membro di CToolTippedListBox) che
avesse lo stile WS_EX_TOOLWINDOW.
voi quale mi consigliate tra queste due? oppure è meglio che ritorno sulla
strada consigliata da Alex e gestire il tooltip dal dialogo in cui la
Listbox è contenuta?
Facendo delle prove ho notato inoltre che la funzione:
int CToolTippedListBox::OnCreate(LPCREATESTRUCT lpCreateStruct)
di questa mia classe non viene mai chiamata (la macro nella message map è
presente).
Forse questo dipende dal fatto che la mia risorsa dialogo contiene una
listbox che è associata ad una variabile di tipo CToolTippedListBox? oppure
dal fatto che il dialogo (e la listbox) sono creati partendo dal file delle
risorse?
scusate per la lunghezza e per il numero infito delle mie domande....
;-))))


Listbox č contenuta?
Delle due, la seconda, ma non sono sicuro che il WS_EX_TOOLWINDOW
sia neppure particolarmente appropriato.  Comunque, incapsulare il
concetto in una stringa derivata da CListBox e` piu` riusabile e quindi
migliore -- la classe di "lista arricchita" ti verra` comoda in futuro in
altri dialoghi (e cosi` pure quella di "mostra testo" ti verra` comoda per
altri controlli diversi da list-box).
Ecco dunque uno scheletro di impostazione.  Nel dialogo, ci limitiamo
a: piazzare col dialog editor un normale oggetto listbox; includere
l'header file della classe CMyList (che andremo poi a costruire);
definire un membro dati
        CMyList m_list;
e connetterlo e usarlo come si fa di solito, cioe` in DoDataExchange
aggiungiamo la riga (se IDC_LIST1 e` l'ID del controllo-listbox):
     DDX_Control(pDX, IDC_LIST1, m_list);
e in OnInitDialog popoliamo la lista, tipo:
    m_list.AddString("Una prima riga, che e` piuttosto lunga");
    m_list.AddString("Una seconda riga, che e` lunga anch'essa");
    m_list.AddString("Una corta");
    m_list.AddString("Un'ultima riga, lunga quanto basta");
Nel dialogo non ci sara` altro che questo (e/o altri normali usi
del controllo listbox) -- il lavoro "speciale" dovra` farlo CMyList.
La CMyList la costruiamo con il wizard, derivando la classe da
CListBox, e aggiungendo due membri dati:
    int m_currItem;              // inizializzare nel ctor a -1
    CPoptext* m_pFloat;    // inizializzare nel ctor a 0
la gestione di OnMouseMove e OnTimer (almeno, questo e`
un possibile approccio), e una funzione-membro:
     void Reminder(int itemId);
La OnMouseMove puo` essere qualcosa del tipo:
void CMyList::OnMouseMove(UINT nFlags, CPoint point)
{
    BOOL bOutside;
    int idItem = ItemFromPoint(point, bOutside);
    if(bOutside) {
        // il mouse e` uscito, togliamoci dai piedi
        m_currItem = -1;
        ReleaseCapture();
        KillTimer(100);
        Reminder(-1);
    } else if(idItem != m_currItem) {
        // il mouse e` entrato, o ha cambiato item
        m_currItem = idItem;
        SetCapture();
        SetTimer(100, 300, 0);
    }
}
basata su una mouse capture per scoprire l'uscita
del mouse dal controllo listbox, e sull'uso di un breve
ritardo (qui ho scelto 0.3 secondi) prima di mostrare
il testo-flottante, per evitare apparizioni "fantasma"
quando il mouse passa di li` "di passaggio" (cosi`
fanno i tooltip-control; naturalmente, si puo` scegliere
un approccio diverso).
L'OnTimer e` d'altronde semplicissima:
void CMyList::OnTimer(UINT nIDEvent)
{
    Reminder(m_currItem);
}
e il vero lavoro lo fa la Reminder -- mostrare un testo
flottante, o nasconderlo se chiamata con argomento -1.
Anche lei, comunque, non e` terribilmente complessa,
perche` fa conto di potere usare un opportuno oggetto
di classe CPoptext per fare il vero lavoro.  Eccola:
void CMyList::Reminder(int itemId)
{
    if(!m_pFloat) {
        // creiamo il floating-text, se occorre
        m_pFloat = new CPoptext();
    }
    if(itemId<0) {
        // ripuliamo lo schermo dal testo
        m_pFloat->ShowWindow(SW_HIDE);
    } else {
        // la stringa da mostrare
        CString cs;
        GetText(itemId, cs);
        // il rettangolo in cui mostrarla
        CRect r;
        GetItemRect(itemId, &r);
        ClientToScreen(&r);
        // deleghiamo il display del testo
        m_pFloat->ShowText(cs, r);
    }
}
Come vediamo, il punto cruciale della classe CPoptext
e` il metodo ShowText, che prende una stringa ed un
rettangolo e mostra la stringa in quel rettangolo.
La classe CPoptext la deriviamo direttamente da CWnd,
aggiungendo solo un override di PostNcDestroy che fa
la "delete this" (CWnd di per se` non lo farebbe), una
OnPaint, e, naturalmente, la ShowText suddetta.
Nel costruttore di CPoptext registriamo la window class
necessaria e procediamo anche alla creazione:
CPoptext::CPoptext()
{
    // registriamo la classe
    CString className = AfxRegisterWndClass(
        CS_SAVEBITS, 0, WHITE_BRUSH);
    // creiamo la finestra (per ora non visibile)
    CreateEx(
        WS_EX_STATICEDGE|WS_EX_TOPMOST,
        className, "",
        WS_BORDER|WS_POPUP,
        0, 0, 0, 0, 0, 0);
}
L'OnPaint e` molto semplice, deve solo mostrare
il "testo della finestra", una sola riga, nel client rectangle:
void CPoptext::OnPaint()
{
    CPaintDC dc(this); // il device context del paint
    // troviamo il ns rettangolo e il ns testo
    CRect rect;  GetClientRect(&rect);
    CString text; GetWindowText(text);
    // disegniamo il testo nel rettangolo
    dc.DrawText(text, &rect, DT_SINGLELINE);
}
e veniamo finalmente alla ShowText:
void CPoptext::ShowText(const CString& s, CRect& r)
{
    // misuriamo il testo da mostrare
    CClientDC dc(this);
    CSize siz = dc.GetTextExtent(s);
    // dimensioniamo di conseguenza il rettangolo
    r.right  = r.left+
siz.cx
;
    r.bottom = r.top+
siz.cy
;
    r.InflateRect(3,2);    // qui, un po' di empirismo...
    // settiamo il testo e ne garantiamo il display
    SetWindowText(s);
    MoveWindow(&r);
    Invalidate();
    ShowWindow(SW_SHOW);
}
Questo e` solo un abbozzo, naturalmente, quel che
si puo` mettere insieme e far funzionare in un'oretta,
e ci sono parecchi dettagli da regolare a piacere (tipo
il fatto che, ora come ora, il testo flottante appare
comunque, anche se la listbox mostrava gia` tutto
l'item -- sarebbe meglio determinare questo fatto e
mostrare il testo flottante solo se serve, cioe` se il
testo dell'item come attualmente mostrato e` tronco).
Ma penso che possa essere una base decente da
cui partire per la funzionalita` che avevi in mente.
Facci sapere...!
Alex

funziona tutto benissimo.... grazie.
Prego, comunque non sono pienamente soddisfatto del
funzionamento della finestra "tooltip" (bubble).  Ce n'e`
una che penso sia migliore, piuttosto generica e riusabile,
come parte delle cosiddette "window foundation classes",
una miscellanea di estensioni gratuite a MFC che trovi a:
http://ourworld.compuserve.com/homepages/Sam_Blackburn/wfc.htm
Cerca CBubble (hpp e cpp), a parte alcune macro di diagnostica
sono autosufficienti (non richiedono altre parti delle wfc).
[NB: non confondiamo le wfc di Sam Blackburn con le WFC della
Microsoft, che usciranno come parte del Developer Studio 6.0 a
Settembre o giu` di li`...!-)]
Alex