Owner-drawing: ListBox with variable item height

Helpful script writing tricks and HowTo's
just me
Posts: 9450
Joined: 02 Oct 2013, 08:51
Location: Germany

Owner-drawing: ListBox with variable item height

24 Oct 2013, 11:49

I'm not sure if this may be called a tutorial or demo, because my limited English might prevent me from explaining things detailed enough, but now it's here though.
Well, but how to do it?

Of course we have to set the LBS_OWNERDRAWVARIABLE (0x0020) and LBS_HASSTRINGS (0x0040) styles, and then?
MSDN wrote:
  • The owner of an owner-drawn list box can process a WM_MEASUREITEM message to specify the dimensions of list items.
  • The owner of an owner-drawn list box must process the WM_DRAWITEM message.
Just two messages, seems easy.

WM_MEASUREITEM:
Actually it isn't as difficult as it seems on the first look. In contrast to list boxes with the LBS_OWNERDRAWFIX (0x0010) style, WM_MEASUREITEM is called once for each item, and the font of the control and the text of the items are already assigned. You can even send a LB_GETITEMRECT message to get width of the item. So the height can be determined 'easily' using the DRAWTEXT() function with the DT_WORDBREAK and DT_CALCRECT flags.
When WM_MEASUREITEM is sent the first time, the control cannot know, whether it has to show a vertical scroll bar. To ensure that all items are calculated using the same width, set the LBS_DISABLENOSCROLL (0x1000) and WS_VSCROLL (0x200000) styles to make the vertical scroll bar permanently visible, if it might/will be shown.

WM_DRAWITEM:
If WM_MEASUREITEM has been processed successfully, it's also 'easy' to draw the text. You can retrieve the assigned text sendingLB_GETTEXTLEN and LB_GETTEXT messages. The DRAWITEMSTRUCT contains the item's rectangle. So DRAWTEXT() with DT_WORDBREAK will do the job.
Some additional work is needed to draw the selected items, but it's feasible. That's it!

The following example does the whole job. It contains four LBODVAR_ functions. The purpose of LBODVAR_MeasureItem() and LBODVAR_DrawItem() should be clear. LBODVAR_Init() activates the message handling of WM_MEASUREITEM and WM_DRAWITEM at load-time. LBODVAR_GetHeightByRows() can by used to determing the control's height appropriate to the passed number of items like the r option of Gui, Add, ....

Code: Select all

#NoEnv
SetBatchLines, -1
WM_MEASUREITEM := 0x002C
; ----------------------------------------------------------------------------------------------------------------------
; ListBox styles
; LBS_OWNERDRAWVARIABLE = 0x0020, LBS_HASSTRINGS = 0x0040, LBS_DISABLENOSCROLL := 0x1000, WS_VSCROLL := 0x200000
LBODStyles := "+0x201060"
; List
Items := "
(Join|
Lorem ipsum dolor sit amet consectetuer.
Rutrum laoreet ligula sem adipiscing adipiscing mattis Cras Curabitur lacus dignissim.
Nibh vitae auctor Quisque orci ut a est eros consequat.
Sed tincidunt augue eget Vestibulum rutrum Nulla tincidunt orci odio nibh.
Et id sit pharetra Praesent sodales odio interdum eleifend velit Maecenas.
Id turpis senectus Curabitur wisi eleifend libero Pellentesque leo sagittis Quisque.
Eu pretium volutpat Vivamus pellentesque tortor sem massa convallis lacus consectetuer.
Volutpat odio vitae odio Suspendisse risus congue eu Curabitur dapibus justo.
Integer venenatis rhoncus nibh dictumst nibh faucibus adipiscing elit dictumst sit.
Justo eros lorem.
At interdum Mauris justo Quisque Vestibulum tincidunt tempus vel nec Morbi.
)"
; ----------------------------------------------------------------------------------------------------------------------
Gui, +LastFound
Gui, Margin, 20, 20
Gui, Add, Text, w300, Common ListBox
Gui, Add, Text, ym w300, Owner-drawn variable ListBox
Gui, Font, % (FontOptions := "s12"), % (FontName := "Arial")
Gui, Add, ListBox, xm y+5 w300 r6 cNavy Choose1, %Items%
H := LBODVAR_GetHeightByRows(6, FontOptions, FontName)
Gui, Add, ListBox, x+20 yp w300 h%H% hwndHLB2 vLBVar cNavy gSubLBVar %LBODStyles% Choose1, %Items%
Gui, Show, , ODLBVAR
WinSet, Redraw
OnMessage(WM_MEASUREITEM, "") ; message handling isn't needed any more after the last control is created
Return
; ----------------------------------------------------------------------------------------------------------------------
GuiClose:
ExitApp
; ----------------------------------------------------------------------------------------------------------------------
SubLBVar:
   GuiControlGet, LBVar
   ToolTip, %LBVar%
   SetTimer, KillTT, -1000
Return
KillTT:
   ToolTip
Return
;=======================================================================================================================
; Owner drawing ========================================================================================================
;=======================================================================================================================
;=======================================================================================================================
LBODVAR_GetHeightByRows(Rows, FontOptions := "", FontName := "") {
;=======================================================================================================================
   ; -------------------------------------------------------------------------------------------------------------------
   ; Rows         -  number of visible rows
   ; FontOptions  -  font options like Gui, Font, Options
   ; FontName     -  font name like Gui, Font, ..., FontName
   ; -------------------------------------------------------------------------------------------------------------------
   Gui, LBODVAR_GetHeight_Gui:Font, %FontOptions%, %FontName%
   Gui, LBODVAR_GetHeight_Gui:Add, ListBox, r%Rows% hwndHLB
   GuiControlGet, P, LBODVAR_GetHeight_Gui:Pos, %HLB%
   Gui, LBODVAR_GetHeight_Gui:Destroy
   Return PH
}
;=======================================================================================================================
LBODVAR_Init() { ; processed at load-time
;=======================================================================================================================
   ; WM_MEASUREITEM = 0x002C, WM_DRAWITEM = 0x002B
   Static MI := OnMessage(0x002C, "LBODVAR_MeasureItem") ; sets the message handler for WM_MEASUREITEM
   Static DI := OnMessage(0x002B, "LBODVAR_DrawItem")    ; sets the message handler for WM_DRAWITEM
}
;=======================================================================================================================
LBODVAR_MeasureItem(WP, LP, M, H) { ; called by system
;=======================================================================================================================
   ; -------------------------------------------------------------------------------------------------------------------
   ; Owner-Drawn ListBox -> http://msdn.microsoft.com/en-us/library/hh298352(v=vs.85).aspx
   ; WM_MEASUREITEM      -> http://msdn.microsoft.com/en-us/library/bb775925(v=vs.85).aspx
   ; MEASUREITEMSTRUCT   -> http://msdn.microsoft.com/en-us/library/bb775804(v=vs.85).aspx
   ; -------------------------------------------------------------------------------------------------------------------
   ; LP -> MEASUREITEMSTRUCT offsets
   Static offType := 0, offItem := 8, offHeight := 16
   ; ListBox messages
   Static LB_GETITEMRECT := 0x0198, LB_GETTEXT := 0x0189, LB_GETTEXTLEN := 0x018A
   ; Other messages
   Static WM_GETFONT := 0x31
   ; Styles
   Static OWNERDRAWVARIABLE := 0x0020
   ; DrawText format flags
   Static DT := 0x0410 ; DT_WORDBREAK = 0x10, DT_CALCRECT = 0x0400
   ; Control related
   Static HOD := 0, HFONT := 0
   ; -------------------------------------------------------------------------------------------------------------------
   HWND := DllCall("User32.dll\GetDlgItem", "Ptr", H, "Int", WP, "UPtr")
   If (HWND <> HOD) {
      SendMessage, %WM_GETFONT%, 0, 0, , ahk_id %HWND%
      HFONT := ErrorLevel
      WinGetClass, Class, ahk_id %HWND%
      ControlGet, Styles, Style, , , ahk_id %HWND%
      If (Class <> "ListBox") || !(Styles & OWNERDRAWVARIABLE)
         Return
      HOD := HWND
   }
   Item := NumGet(LP + 0, offItem, "Int")
   SendMessage, %LB_GETTEXTLEN%, %Item%, 0, , ahk_id %HOD%
   Len := ErrorLevel
   VarSetCapacity(ItemText, Len << 2, 0)
   SendMessage, %LB_GETTEXT%, %Item%, &ItemText, , ahk_id %HOD%
   VarSetCapacity(RECT, 16, 0)
   SendMessage, %LB_GETITEMRECT%, %Item%, &RECT, , ahk_id %HOD%
   NumPut(NumGet(RECT, 0, "Int") + 2, RECT, 0, "Int") ; 2 pixel text indent
   HDC := DllCall("User32.dll\GetDC", "Ptr", HOD, "UPtr")
   OFONT := DllCall("Gdi32.dll\SelectObject", "Ptr", HDC, "Ptr", HFONT)
   Height := DllCall("User32.dll\DrawText", "Ptr", HDC, "Ptr", &ItemText, "Int", Len, "Ptr", &RECT, "UInt", DT)
   DllCall("Gdi32.dll\SelectObject", "Ptr", HDC, "Ptr", OFONT)
   DllCall("User32.dll\ReleaseDC", "Ptr", HOD, "Ptr", HDC)
   NumPut(Height, LP + 0, offHeight, "Int")
   Return True
}

;=======================================================================================================================
LBODVAR_DrawItem(WP, LP, M, H) { ; called by system
;=======================================================================================================================
   ; -------------------------------------------------------------------------------------------------------------------
   ; Owner-Drawn ListBox -> http://msdn.microsoft.com/en-us/library/hh298352(v=vs.85).aspx
   ; WM_DRAWITEM         -> http://msdn.microsoft.com/en-us/library/bb775923(v=vs.85).aspx
   ; DRAWITEMSTRUCT      -> http://msdn.microsoft.com/en-us/library/bb775802(v=vs.85).aspx
   ; -------------------------------------------------------------------------------------------------------------------
   ; LP / DRAWITEMSTRUCT offsets
   Static offItem := 8, offAction := offItem + 4, offState := offAction + 4, offHWND := offState + A_PtrSize
        , offDC := offHWND + A_PtrSize, offRECT := offDC + A_PtrSize, offData := offRECT + 16
   ; Styles
   Static OWNERDRAWVARIABLE := 0x0020
   ; ListBox messages
   Static LB_GETTEXT := 0x0189, LB_GETTEXTLEN := 0x018A
   ; Owner Draw Actions
   Static ODA_DRAWENTIRE := 0x0001, ODA_SELECT := 0x0002, ODA_FOCUS := 0x0004
   ; Owner Draw States
   Static ODS_SELECTED := 0x0001, ODS_FOCUS := 0x0010
   ; Draw text format flags
   Static DT_WORDBREAK := 0x10
   ; Selection
   Static SelBg := DllCall("User32.dll\GetSysColor", "Int", 13, "UInt") ; COLOR_HIGHLIGHT
   Static SelTx := DllCall("User32.dll\GetSysColor", "Int", 14, "UInt") ; COLOR_HIGHLIGHTTEXT
   ; Control specific
   Static HOD := 0
   ; -------------------------------------------------------------------------------------------------------------------
   Critical ; may help in case of drawing issues
   HWND := NumGet(LP + offHWND, 0, "UPtr")
   HDC := NumGet(LP + offDC, 0, "UPtr")
   If (HWND <> HOD) {
      WinGetClass, Class, ahk_id %HWND%
      ControlGet, Styles, Style, , , ahk_id %HWND%
      If (Class <> "ListBox") || !(Styles & OWNERDRAWVARIABLE)
         Return
      HOD := HWND
   }
   CtlBg := DllCall("Gdi32.dll\GetBkColor", "Ptr", HDC, "UInt")
   CtlTx := DllCall("Gdi32.dll\GetTextColor", "Ptr", HDC, "UInt")
   Item := NumGet(LP + offItem, 0, "Int")
   Action := NumGet(LP + offAction, 0, "UInt")
   State := NumGet(LP + offState, 0, "UInt")
   RECT := LP + offRECT
   SendMessage, %LB_GETTEXTLEN%, %Item%, 0, , ahk_id %HOD%
   Len := ErrorLevel
   VarSetCapacity(ItemText, Len << 2, 0)
   SendMessage, %LB_GETTEXT%, %Item%, &ItemText, , ahk_id %HOD%
   BgColor := State & ODS_SELECTED ? SelBg : CtlBg
   TxColor := State & ODS_SELECTED ? SelTx : CtlTx
   Brush := DllCall("Gdi32.dll\CreateSolidBrush", "UInt", BgColor, "UPtr")
   DllCall("User32.dll\FillRect", "Ptr", HDC, "Ptr", RECT, "Ptr", Brush)
   DllCall("Gdi32.dll\DeleteObject", "Ptr", Brush)
   DllCall("Gdi32.dll\SetTextColor", "Ptr", HDC, "UInt", TxColor)
   DllCall("Gdi32.dll\SetBkMode", "Ptr", HDC, "Int", 1) ; TRANSPARENT
   NumPut(NumGet(RECT + 0, 0, "Int") + 2, RECT + 0, 0, "Int") ; 2 pixel text indent
   DllCall("User32.dll\DrawText", "Ptr", HDC, "Ptr", &ItemText, "Int", Len, "Ptr", RECT, "UInt", DT_WORDBREAK)
   NumPut(NumGet(RECT + 0, 0, "Int") - 2, RECT + 0, 0, "Int")
   DllCall("Gdi32.dll\SetTextColor", "Ptr", HDC, "UInt", CtlTx)
   If (State & ODS_FOCUS)
      DllCall("User32.dll\DrawFocusRect", "Ptr", HDC, "Ptr", RECT)
   Return True
}

Return to “Tutorials (v1)”

Who is online

Users browsing this forum: No registered users and 43 guests