ListView with Background. Successful.

J

Jason

Hello all,

I have been looking around Google Groups trying to find out how to
enable background images with System.Windows.Forms.ListView only to
discover most people are having a lot of difficulty. After several hours
hunting around with Lutz Roeder's .NET Reflector I have created an
inherited class (source code at bottom) to cause the usually ignored
BackgroundImage property of the ListView to become enabled. I noticed
that when the ListView had UserPaint enabled it would automatically draw
the background but then I'd need to manually draw the items. So I just
caught the WM_ERASEBKGND message, enabled UserPaint for the background
stage, then disabled UserPaint for the foreground stage. No WinAPI was
required. At the moment the background is always aligned top-left and
tiled to fill the ListView but this suits my needs so I'll leave the
tiling adjustment until another day. By the way, I only use ListViews in
Report/Detail view mode so I haven't tested it with other views.
Hopefully this will be useful to many other fellow programmers.

The remaining problem is that an empty list shows the full background
image but as ListViewItems are added the rows are always drawn with a
solid BackColor hiding the background image behind the text. Again, this
is not a major problem for my application but it would be nice to make
the row BackColor transparent so the background image shows like a
watermark behind any text. Setting the ListView or the ListViewItem's
BackColor to Color.Transparent is rejected with an exception. If I use
MyBase.SetStyle(ControlStyles.SupportsTransparentBackColor, True) in my
extended class's constructor I can set the BackColor to Transparent but
it still draws as white. From what I've discovered about Microsoft's
ListView implementation is that I will probably need to catch WM_NOTIFY
messages of type NM_CUSTOMDRAW and either set the BackColor
appropriately there or draw each row's background manually. I have no
experience with the CustomDraw service and there are so many options
that I don't know where to start. Perhaps someone who is more familiar
with this aspect of Common Controls can provide some assistance.

Thank you in advance,

- Jason

''' CODE BEGINS
'Just drop a ListViewWithBackground onto a form and set the
'BackgroundImage property during the form's Load event.
Public Class ListViewWithBackground
Inherits System.Windows.Forms.ListView

Private Const WM_ERASEBKGND As Int32 = &H14

Protected Overrides Sub WndProc(ByRef m As
System.Windows.Forms.Message)
Select Case m.Msg
Case WM_ERASEBKGND
Dim styleUserPaint As Boolean =
Me.GetStyle(ControlStyles.UserPaint)
Dim styleAllPainting As Boolean =
Me.GetStyle(ControlStyles.AllPaintingInWmPaint)
Me.SetStyle(ControlStyles.UserPaint, True)
Me.SetStyle(ControlStyles.AllPaintingInWmPaint, False)
MyBase.WndProc(m)
Me.SetStyle(ControlStyles.UserPaint, styleUserPaint)
Me.SetStyle(ControlStyles.AllPaintingInWmPaint,
styleAllPainting)
Case Else
MyBase.WndProc(m)
End Select
End Sub

End Class
''' CODE ENDS
 
H

Herfried K. Wagner [MVP]

* Jason said:
Hello all,

I have been looking around Google Groups trying to find out how to
enable background images with System.Windows.Forms.ListView only to
discover most people are having a lot of difficulty. After several
hours hunting around with Lutz Roeder's .NET Reflector I have created
an inherited class (source code at bottom) to cause the usually
ignored BackgroundImage property of the ListView to become enabled. I
noticed that when the ListView had UserPaint enabled it would
automatically draw the background but then I'd need to manually draw
the items. So I just caught the WM_ERASEBKGND message, enabled
UserPaint for the background stage, then disabled UserPaint for the
foreground stage. No WinAPI was required. At the moment the background
is always aligned top-left and tiled to fill the ListView but this
suits my needs so I'll leave the tiling adjustment until another
day. By the way, I only use ListViews in Report/Detail view mode so I
haven't tested it with other views. Hopefully this will be useful to
many other fellow programmers.

You can archieve the same using p/invoke:

The .NET 1.1 listview control doesn't have a property to set its background
image. This can be done using the code below:

\\\
Imports System.Runtime.InteropServices
..
..
..
Private Declare Auto Function SendMessage Lib "user32.dll" ( _
ByVal hWnd As IntPtr, _
ByVal wMsg As Int32, _
ByVal wParam As Int32, _
ByRef lParam As LVBKIMAGE _
) As Boolean

Private Const LVM_FIRST As Int32 = &H1000

' Requires 'CoInitialize' to be called.
Private Const LVM_SETBKIMAGE As Int32 = (LVM_FIRST + 68)

Private Const LVBKIF_SOURCE_URL As Int32 = &H2
Private Const LVBKIF_STYLE_TILE As Int32 = &H10

<StructLayout(LayoutKind.Sequential)> _
Private Structure LVBKIMAGE
Public ulFlags As Int32
Public hbm As IntPtr
Public pszImage As String
Public cchImageMax As Int32
Public xOffsetPercent As Int32
Public yOffsetPercent As Int32
End Structure

Public Function SetListViewBackgroundImage( _
ByVal ListView As ListView, _
ByVal BackgroundImageFileName As String _
) As Boolean

' Set background image.
Dim lbi As LVBKIMAGE
With lbi
.pszImage = BackgroundImageFileName
.cchImageMax = Len(.pszImage)
.ulFlags = LVBKIF_SOURCE_URL Or LVBKIF_STYLE_TILE
End With
Return SendMessage(ListView.Handle, LVM_SETBKIMAGE, 0, lbi)
End Function
///

Notice that the 'CoInitialize' function must be called before setting the
background image.

Usage:

\\\
Debug.Assert( _
SetListViewBackgroundImage(Me.ListView1, "C:\WINDOWS\ANGLER.BMP") _
)
///
The remaining problem is that an empty list shows the full background
image but as ListViewItems are added the rows are always drawn with a
solid BackColor hiding the background image behind the text. Again,
this is not a major problem for my application but it would be nice to
make the row BackColor transparent so the background image shows like
a watermark behind any text. Setting the ListView or the
ListViewItem's BackColor to Color.Transparent is rejected with an
exception.

You will find some VB6 code to do that below. Set the backcolor to 'CLR_NONE':

\\\

' ListView messages.
Private Const LVM_FIRST = &H1000
Private Const LVM_GETTEXTCOLOR = (LVM_FIRST + 35)
Private Const LVM_SETTEXTCOLOR = (LVM_FIRST + 36)
Private Const LVM_GETTEXTBKCOLOR = (LVM_FIRST + 37)
Private Const LVM_SETTEXTBKCOLOR = (LVM_FIRST + 38)

' Transparent, "no color".
Private Const CLR_NONE = &HFFFFFFFF

Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Declare Function InvalidateRect Lib "user32" (ByVal hWnd As Long, lpRect As Any, ByVal bErase As Long) As Long
Private Declare Function UpdateWindow Lib "user32" (ByVal hWnd As Long) As Long

'***********************************
' ListView Routines.
'***********************************

Private Sub UpdateListView(lnghWnd As Long)

' Add a rectangle to the listview's update region. This is the portion of the window's client area
' that must be redrawn. The 0 parameters tells it to redraw the entire client area.
Call InvalidateRect(lnghWnd, ByVal 0&, True)

' Send a WM_PAINT message to the listview to force it to redraw itself.
Call UpdateWindow(lnghWnd)
End Sub

Private Function ListViewSetTextBkColor(lnghWnd As Long, lngClrTextBk As Long) As Long
ListViewSetTextBkColor = SendMessage(lnghWnd, LVM_SETTEXTBKCOLOR, 0&, lngClrTextBk)

Call UpdateListView(lnghWnd)
End Function

Private Function ListViewGetTextBkColor(lnghWnd As Long) As Long
ListViewGetTextBkColor = SendMessage(lnghWnd, LVM_GETTEXTBKCOLOR, 0, 0)
End Function
///
 
J

Jason

I originally began with a similar example using LVM_SETBKIMAGE and
LVM_SETTEXTBKCOLOR to set the background images but something about the
way the .NET ListView is implemented seemed to stop it from working.
Maybe I was doing it wrong however. Also, the LVM_SETBKIMAGE requires
the background image to be available as a file but I'm using embedded
resources.

If your method works for you let me know but it didn't work for me. I
have found some CustomDraw examples on vbAccelerator.com and I think I
am making progress. I'll post my solution as soon as it works.

Thanks,

- Jason
 
J

Jason

Hello again,

I have solved the transparent row problem. Microsoft's implementation of
the .NET ListView catches the WM_NOTIFY message, reposts the message as
(WM_NOTIFY Or 8192), then processes the NM_CUSTOMDRAW message for each
row item and sub item, setting the forecolor, backcolor, and font.

My code also catches the NM_CUSTOMDRAW message at the right time, allows
the Microsoft implementation to set the correct fonts and forecolors
then my code simply overrides the backcolor setting with transparent.

Unfortunately there is now another hitch. If I select, then deselect a
row, the unselected row maintains the blue-ish selection background. If
I remove a row, the background image is shifted up in the first column.
If I pass another window over the ListView, the ListView redraws itself
completely and the background is restored as is the color of the
unselected row. This would suggest that I need to force the ListView to
redraw itself after a selection or removal operation. I will look into
that but until then, here is the latest code.

- Jason

''' CODE BEGINS
'Just drop a ListViewWithBackground onto a form and set the
'BackgroundImage property during the form's Load event.
Imports System.Runtime.InteropServices

Public Class ListViewWithBackground
Inherits System.Windows.Forms.ListView

Private Const WM_ERASEBKGND As Int32 = &H14
Private Const WM_NOTIFY As Int32 = &H4E
Private Const WM_REFLECTNOTIFY As Int32 = &H2000 Or WM_NOTIFY

Private Const NM_CUSTOMDRAW As Int32 = -12

Private Const CDDS_PREPAINT As Int32 = &H1
Private Const CDDS_ITEM As Int32 = &H10000
Private Const CDDS_ITEMPREPAINT As Int32 = CDDS_ITEM Or CDDS_PREPAINT

Private Const CLR_NONE As Int32 = &HFFFFFFFF

<StructLayout(LayoutKind.Sequential)> _
Private Structure RECT

Public Sub New(ByVal left As Integer, ByVal top As Integer,
ByVal right As Integer, ByVal bottom As Integer)
Me.left = left
Me.top = top
Me.right = right
Me.bottom = bottom
End Sub

Public Shared Function FromXYWH(ByVal x As Integer, ByVal y As
Integer, ByVal width As Integer, ByVal height As Integer) As RECT
Return New RECT(x, y, x + width, y + height)
End Function

Public left As Integer
Public top As Integer
Public right As Integer
Public bottom As Integer
End Structure

<StructLayout(LayoutKind.Sequential)> _
Private Structure NMHDR
Public hwndFrom As IntPtr
Public idFrom As Integer
Public code As Integer
End Structure

<StructLayout(LayoutKind.Sequential)> _
Private Structure NMCUSTOMDRAW
Public nmcd As NMHDR
Public dwDrawStage As Integer
Public hdc As IntPtr
Public rc As RECT
Public dwItemSpec As Integer
Public uItemState As Integer
Public lItemlParam As IntPtr
End Structure

<StructLayout(LayoutKind.Sequential)> _
Private Structure NMLVCUSTOMDRAW
Public nmcd As NMCUSTOMDRAW
Public clrText As Integer
Public clrTextBk As Integer
Public iSubItem As Integer
End Structure

Public Sub New()
MyBase.New()
MyBase.SetStyle(ControlStyles.SupportsTransparentBackColor, True)
End Sub

Protected Overrides Sub WndProc(ByRef m As
System.Windows.Forms.Message)
Select Case m.Msg
Case WM_ERASEBKGND
Dim styleUserPaint As Boolean =
Me.GetStyle(ControlStyles.UserPaint)
Dim styleAllPainting As Boolean =
Me.GetStyle(ControlStyles.AllPaintingInWmPaint)
Me.SetStyle(ControlStyles.UserPaint, True)
Me.SetStyle(ControlStyles.AllPaintingInWmPaint, False)
MyBase.WndProc(m)
Me.SetStyle(ControlStyles.UserPaint, styleUserPaint)
Me.SetStyle(ControlStyles.AllPaintingInWmPaint,
styleAllPainting)
Case WM_REFLECTNOTIFY
MyBase.WndProc(m)
Dim nmh As NMHDR
nmh = CType(Marshal.PtrToStructure(m.LParam,
nmh.GetType), NMHDR)
If nmh.code = NM_CUSTOMDRAW Then
Dim nmlvcd As NMLVCUSTOMDRAW
nmlvcd = CType(Marshal.PtrToStructure(m.LParam,
nmlvcd.GetType), NMLVCUSTOMDRAW)
HandleCustomDraw(nmlvcd, m)
Marshal.StructureToPtr(nmlvcd, m.LParam, True)
End If
Case Else
MyBase.WndProc(m)
End Select
End Sub

Private Sub HandleCustomDraw(ByRef nmlvcd As NMLVCUSTOMDRAW, ByRef
m As System.Windows.Forms.Message)
Select Case nmlvcd.nmcd.dwDrawStage
Case CDDS_PREPAINT, CDDS_ITEMPREPAINT
nmlvcd.clrTextBk = CLR_NONE
End Select
End Sub

End Class
''' CODE ENDS
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top