Refining a custom listbox control - vb.net

I have made a sort of custom list box and have included the ability to add an image to each item.
I want to be able to change these images one by one at will and am not too sure on how to go about it. At the moment you can only select which image you would like in every item.
Public Class CustomListBox
Public label As Label
Public pic As PictureBox
Public panel As Panel
Public itemID As String
Public itemCollection As New Collection
Public bgColor As Color
Public txtEnterColor As Color = Color.FromArgb(80, 80, 80)
Public txtColor As Color = Color.FromArgb(150, 150, 150)
Public bgEntercolor As Color = Color.FromArgb(230, 230, 230)
Public x, y, paddingInt As Integer
Public itemHeight As Integer = 40
Public image As Image = My.Resources.FavNone
Public Event Item_Clicked()
Private Property ItemBackColor As Color
Get
Return BackColor
End Get
Set(ByVal value As Color)
bgColor = value
End Set
End Property
Private Property ItemPadding As Padding
Get
Return Padding
End Get
Set(ByVal value As Padding)
Padding = value
End Set
End Property
Public Property HoverBackColor As Color
Get
Return bgEntercolor
End Get
Set(ByVal value As Color)
bgEntercolor = value
End Set
End Property
Public Property ItemImage As Image
Get
Return image
End Get
Set(ByVal value As Image)
image = value
End Set
End Property
Public Property HoverTextColor As Color
Get
Return txtEnterColor
End Get
Set(ByVal value As Color)
txtEnterColor = value
End Set
End Property
Public Property TextColor As Color
Get
Return txtColor
End Get
Set(ByVal value As Color)
txtColor = value
End Set
End Property
Public Property TrueItemHeight As Integer
Get
Return itemHeight
End Get
Set(ByVal value As Integer)
itemHeight = value
End Set
End Property
Public Sub UpdateItems()
For Each item As String In itemCollection
label = New Label
pic = New PictureBox
panel = New Panel
With pic
.Width = itemHeight
.Height = itemHeight
.SizeMode = PictureBoxSizeMode.Zoom
.Image = image
End With
With label
.BackColor = (bgColor)
.ForeColor = (txtColor)
.Width = Me.Width - itemHeight
.Height = itemHeight
.Tag = item
.Height = itemHeight
.Padding = ItemPadding
.Text = item
.Left = itemHeight
.TextAlign = ContentAlignment.MiddleLeft
AddHandler label.MouseEnter, AddressOf Item_Enter
AddHandler label.MouseLeave, AddressOf Item_Leave
AddHandler label.MouseUp, AddressOf Item_Mousedown
End With
With panel
.Location = New Point(x, y)
.Width = Me.Width
.Height = itemHeight
.Controls.Add(pic)
.Controls.Add(label)
y += .Height + paddingInt
End With
Me.Controls.Add(panel)
Next
End Sub
Private Sub Item_Enter(ByVal sender As Label, ByVal e As EventArgs)
sender.BackColor = (bgEnterColor)
sender.ForeColor = (txtEnterColor)
itemID = sender.Tag
End Sub
Private Sub Item_Leave(ByVal sender As Label, ByVal e As EventArgs)
sender.BackColor = (bgColor)
sender.ForeColor = (txtColor)
End Sub
Private Sub Item_Mousedown(ByVal sender As Label, ByVal e As MouseEventArgs)
Select Case e.button
Case Windows.Forms.MouseButtons.Left
RaiseEvent Item_Clicked()
End Select
End Sub
End Class
I know I'll need to add something into the click event of the items.Possible set a variable with the index number of the label, maybe...?
Also, how do I get auto-suggestions when typing the code. For example, I want to be able to type CustomListBox1.itemCollection.add("Text", imageSrc)...etc I just have no idea what to type into Google apart from looking through loads of custom controls until I find one that includes this.
EDIT
I have looked into this custom Listbox.
I tried to add a MouseMove event to each item so thought it would be as easy as placing this:
Private Sub HoverItem(ByVal item As ColorListboxItem, ByVal e As MouseEventArgs)
msgbox(1)
End Sub
...in the "Methods" region and then
AddHandler .mousemove, AddressOf HoverItem
to the "OnDrawItem" sub. Unfortunately for me, it obviously isn't that easy as no msgbox shows.
Could anybody with experience on this control give a bit of insight as to how it works. And maybe an example of the MouseMove event so then I'll get an idea on how to add in more events (Mouseleave, DblClick...etc)

What #JoshMason said is correct for the most part, but if you insist on doing that, then you need not only an indexed collection/array for your items but also a linked associated collection/array for the corresponding images.
So that way the index of the item/image is accessible to you (properly exposed of course) so that you can assign an image to it like you would assign a text to the item(index) and you need to make sure your code accounts for removing an item also removes the corresponding image, but if you link them properly, that should happen automatically.
EDIT: For inspiration, give this a quick look.

That looks familiar - I have something very similar to house and track a bunch of thumbnails. A few things. The way it is written it is more of a ListBox helper than a custom control. There is nothing wrong with that, but if you want it to show up in the toolbox and be more reusable, consider reworking it:
Public Class CustomListBox
Inherits Panel ' or maybe Component, depending....
The way yours is written, you are trying to emulate ListBox functionality by maintaining a collection of labels and picboxes and panels. If you start to think about each panel+picbox+textbox as its OWN integral thing (control), you can internalize some of the functionality at that level (Event processing for instance) and leave the ListBox helper mainly to manage the interactions with the user or app (or go away). I dont think auto-suggest is going to work until it is an actual control or component.
Private WithEvents mLbl As TextBox ' just recently decided to allow text edits
Private WithEvents mPic As PictureBox
Public Sub New(ByVal uniqueName As String)
mLbl = New TextBox
mPic = New PictureBox
Name = uniqueName
.... set required lbl/txt properties
MyBase.Controls.Add(mLbl) ' we inherit from Panel
.... set pic controls
MyBase.Controls.Add(mPic)
...
...
' no need for AddHandler, each Item comes with its own built in event
Private Sub mPic_DClick(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles mPic.DoubleClick, mLbl.DoubleClick
IsSelected = Not _IsSelected
End Sub
After it is created with default or basic props, the class that is creating it sets the unique ones like Text, Image and location on the scrolling panel before adding it to the scrollpanel:
frmMain.pnlImgList.Controls.Add(newImgItem)
IsSelected (above) is a property, that when it changes from False to True, I raise a new event ItemSelected to notify the app/panel housing each "ImgItem" Control. The app need not know whether it was the textbox or pic clicked because ImgItem will handle that (like edit Text). In your case, this could be changing the color when selected/focused etc. (Breaking it down into 2 pieces would eventually let you get rid of that big procedure to create all new items).
What I dont have is any internal collection of these. They are added to a panel on the form and that panel's controls collection serves that purpose. When a new one of these things is added to the form, it DOES have to be hooked up to events using AddHandler to process events like ItemSelected (there is a ControlAdded/ControlRemoved event that makes a nice spot for hooking/unhooking these to the Selected event handler!)
You could do something similar using a scrolling panel of items which looks and acts like a Listbox.
Before you can do things like change an image, you need a unique identifier so you know WHICH picture to change. It looks like you use the text as the name. Depending on how you use it, that might work but if the user can edit the text, they could create a duplicate; and if the name changes you run the risk of loosing track of things. So, consider tagging them with your own unique name that isnt exposed to the user:
' this would be passed to Sub New when creating a new Item
newName = System.Guid.NewGuid.ToString()
The unique ID (Name) is part of the events args in the ItemSelected event, making it easy to find each control on the form:
ThisCtl = pnlItems.Controls(sender.Name)
To change an image, just expose it at the "item" level:
Friend Property Pic() As Bitmap
Get
' I dont recall why this is this way, maybe an artifact
' from trying different things.
Return CType(mPic.BackgroundImage, Bitmap)
End Get
The form or your helper can then change the image:
ThisCtl = pnlItems.Controls(sender.Name)
ThisCtl.Pic = newImage
Or frmName.pnlItems(sender.Name).Pic = newImage
Re your edit: Some of what you want to do with the mouse (Change colors) might be able to be done on the cheap by changing BackColor in mouse events. Some things though might be better handled by making it a proper component so that you can Shadow and OVerride mouse and paint procedures as needed. If you keep the items in an actual ListBOx, you almost certainly are going to have to hook into the DrawItem paint events. Worry about that after you decide whether or not to convert it to a component.
HTH

Related

Combobox custom control with a lid

I have created a custom combobox with a label to cover the combobox (as it's very ugly) when it's not in use. The label, witch is the lid should show the display member. The covering and uncovering forks fine however the text on the label displayed is the previous value and not the current one. Passing over the label with the mouse, triggers the labels mouse enter event and than the display value is correct.
Here is the code for the custom control.
Public Class ComboBoxWithALid
Inherits ComboBox
Private WithEvents Lid As New Label With {.BackColor = Color.LightCyan, .ForeColor = Color.Black,
.TextAlign = ContentAlignment.MiddleCenter}
Protected Overrides Sub OnDataSourceChanged(ByVal e As EventArgs)
MyBase.OnDataSourceChanged(e)
Lid.Location = Location
Lid.Size = Size
Parent.Controls.Add(Lid)
Lid.BringToFront()
End Sub
Private Sub Lid_MouseEnter(sender As Object, e As EventArgs) Handles Lid.MouseEnter
Lid.SendToBack()
End Sub
Protected Overrides Sub OnMouseLeave(e As EventArgs)
MyBase.OnMouseLeave(e)se
Lid.Text = SelectedText
Lid.BringToFront()
End Sub
Protected Overrides Sub OnDropDownClosed(e As EventArgs)
MyBase.OnDropDownClosed(e)
Lid.BringToFront()
Lid.Text = SelectedText
End Sub
End Class
To test the control, drag the control from the tool box to your form and bind the control to any table you have
I tried to use text in place of selected text - same results
I found the solution. Change the move statement to:
Lid.Text = Items(SelectedIndex)(DisplayMember)
and is works.
SelectedText is for the highlighted portion of the text that is currently selected, not the selected item. You probably want something like this:
If SelectedIndex = -1 Then
Lid.Text = String.Empty
Else
Lid.Text = Items(SelectedIndex).ToString()
End If
Yes, you have an annoying control.

Update data bound control when data changes

I have a List(T) that is bound to some controls, a read-only DataGridView, a ComboBox and a few Labels. This works fine, the controls are all populated correctly when the form loads, the Label.Text and DataGridView row focus all change as the ComboBox selection is changed.
But if I change the data in an object on the List the data shown in the controls does not update to reflect the changed data.
My class T implements the INotifyChanged interface and the label control data bindings update mode is set to OnPropertychanged.
I can force the DataGridView to update by calling its Refresh() method, but trying the same for the labels seems to have no effect.
So how can I make changes to the data in the objects in my list update the data shown in the Label controls? Have I done something wrong?
My MRE so far:
Class Form1
' Form1 has a DataGridView, a ComboBox, a Label, a Button and a TextBox
Dim FooList As New List(Of Foo)(3)
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
For Index As Integer = 0 To FooList.Capacity - 1
FooList.Add(New Foo() With {.Bar = Index, .Baz = 0})
Next
' Shows all Bar and Baz
DataGridView1.DataSource = FooList
' User selects Bar value
ComboBox1.DataSource = FooList
ComboBox1.DisplayMember = "Bar"
' Related Baz value shows
Label1.DataBindings.Add(New Binding("Text", FooList, "Baz", DataSourceUpdateMode.OnPropertyChanged))
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
' This is not _actually_ how I'm selecting indexes and changing the data
' But for the MRE it changes the Baz property
'Change the Baz value on the List, should result in Label1 changing
FooList(ComboBox1.SelectedItem.Bar).Baz = TextBox1.Text.Convert.ToUInt16
' Should I even need this when my list objects have INotifyChanged?
DataGridView1.Refresh()
End Sub
End Class
Class Foo
Implements INotifyChanged
Private _bar As UInt16
Private _baz As UInt16
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(ByVal PropertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
End Sub
Property Bar As UInt 16
Get
Return _bar
End Get
Set(value As Byte)
If Not (value = _bar) Then
_bar = Bar
NotifyPropertyChanged("Bar")
End If
End Set
End Property
Property Baz As UInt 16
Get
Return _baz
End Get
Set(value As Byte)
If Not (value = _baz) Then
_baz = Baz
NotifyPropertyChanged("Baz")
End If
End Set
End Property
End Class
One way to have changes in the collection reflected in bound controls, is to "reset" the DataSource:
FooList.Add(New Foo(...))
dgv1.DataSource = Nothing
dgv1.DataSource = FooList
If the control is something like a ListBox, you have to also reset the DisplayMember and ValueMember properties because they get cleared. This is a greasy way to notify controls of changes to the list because many things get reset. In a ListBox for instance, the SelectedItems collection is cleared.
A much better way to let changes to your collection flow thru to controls is to use a BindingList(Of T). Changes to the collection/list (adds, removes) will automatically and instantly be shown in your control.
INotifyPropertyChanged goes one step further. If a property value on an item in the list changes, the BindingList<T> will catch the PropertyChanged events your class raises and "forward" the changes to the control.
To be clear, the BindingList handlea changes to the list, while INotifyPropertyChanged handles changes to the items in the list.
FooList(13).Name = "Ziggy"
Using a List<T> the name change won't show up unless you "reset" the DataSource. Using a BindingList<T> alone, it wont show up right away - when the list changes, it should show up. Implementing INotifyPropertyChanged allows the change to show up right away.
Your code is mostly correct for it, except it is not INotifyChanged. But there is also a shortcut as of Net 4.5:
Private Sub NotifyChange(<CallerMemberName> Optional propname As String = "")
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propname))
End Sub
CallerMemberName is an attribute which allows you to forego actually passing the name; the named ends up being substituted at runtime:
Private _name As String
Public Property Name As String
Get
Return _name
End Get
Set(value As String)
If _name <> value Then
_name = value
NotifyChange()
End If
End Set
End Property
If nothing else it can cut down on copy/paste errors if there are lots of properties to raise events for (ie Bar property using NotifyChange("Foo") because you copied the code from that setter).
I figured I can push the data around the other way. That is, instead of updating the data in the list and then trying to update the controls, I update the control and the List data is updated via the binding.
E.g. My click event from above now becomes:
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Label1.Text = TextBox1.Text
DataGridView1.Refresh()
End Sub
Though TBH Im not a fan of that and I'm still puzzled as to how I could better use the INotifyPropertyChanged interface .

How to make a control to be painted/refreshed properly

I have a control derived from checkbook which I called "SettingBooleanButton", but when any window or dialog is dragged over the control the control keeps signs of the drag
The next image shows the effect of dragging an application window over control
This is the code block that I have for OnPaint()
Public Class SettingBooleanButton
Inherits CheckBox
Private _settingSection As String
Private _settingName As String
Private _associatedSetting As Setting
Public Event StateChange(ByVal affectedSetting As Setting)
Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Appearance = Appearance.Button
FlatStyle = FlatStyle.Flat
TextAlign = ContentAlignment.MiddleCenter
AutoSize = False
End Sub
Public Property SettingSection As String
Get
Return _settingSection
End Get
Set(value As String)
_settingSection = value
End Set
End Property
Public Property SettingName As String
Get
Return _settingName
End Get
Set(value As String)
_settingName = value
End Set
End Property
''' <summary>
''' Sets a boolean value to indicate the initial checked state of the control.
''' </summary>
''' <value>
''' <c>true</c> to set it as [checked state]; otherwise, <c>false</c>.
''' </value>
Public Property CheckedState As Boolean
Get
Return Checked
End Get
Set(value As Boolean)
_associatedSetting = New Setting(_settingSection, _settingName, String.Empty)
RemoveHandler CheckedChanged, AddressOf StateChanged
Checked = value
SetText()
AddHandler CheckedChanged, AddressOf StateChanged
End Set
End Property
Private Sub StateChanged(sender As Object, e As EventArgs)
If IsNothing(_associatedSetting) Then
Return
End If
_associatedSetting.Value = Checked.ToString()
SetText()
RaiseEvent StateChange(_associatedSetting)
End Sub
Public Sub SetText()
If Checked Then
Font = New Font(Font.FontFamily, Font.Size, FontStyle.Bold)
ForeColor = Color.WhiteSmoke
Text = Resource.SettingBooleanButton_TrueState
Else
Font = New Font(Font.FontFamily, Font.Size, FontStyle.Regular)
ForeColor = SystemColors.ControlText
Text = Resource.SettingBooleanButton_FalseState
End If
End Sub
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
MyBase.OnPaint(e)
If Checked Then
ControlPaint.DrawBorder(e.Graphics, e.ClipRectangle, Color.Black, ButtonBorderStyle.Solid)
End If
End Sub
End Class
ControlPaint.DrawBorder(e.Graphics, e.ClipRectangle, ...)
Using e.ClipRectangle like this is a traditional bug in a Paint event handler. It is not a rectangle that matches the border you want to draw. It is only the part of the control that needs to be painted. Which is usually the entire control, but not always. Such as in your case when you drag a window across your control, only the part that is revealed needs to be repainted. So now you are painting the border in the wrong position, producing those black lines.
You only ever use the ClipRectangle if your painting code is expensive and you want to take the opportunity to skip that expensive code when it isn't needed anyway. Which is pretty rare, clipping in Windows is already pretty efficient.
You'll need to pass the actual rectangle of your border. Fix:
ControlPaint.DrawBorder(e.Graphics, Me.ClientRectangle, _
Color.Black, ButtonBorderStyle.Solid)
Sometimes the simplest solutions (or causes) are overlooked.
I have a panel with 15 buttons on it and each has an image. Depending on rows selected from a data grid they all might be enabled or disabled.
It all worked fine except toggling between enabled and disabled was taking 2+ seconds and caused lag when multi-selecting from the data grid.
Tried a few things, then I thought maybe it was something to do with the images.
The images were all in an imagelist and size was set to 24,24 which was a compromise between 32,32 and 16,16. I changed the size in the imagelist to 32,32 as that is the native size of all the images... and shazam!!! All the buttons are basically rendered instantly now. No idea ATM whether being small PNG images makes a difference... but I'm going to convert all the images I have to ICO format.
Also... as all my buttons are on a panel I enable/disable the panel which in turn enables and disables all the children on it.

Button BackColor (seems to) ignore alpha channel

I built a little Tic-Tac-Toe game in winforms using nothing but layout panels, buttons and labels. Each game has 2 players, and each player is associated with a mark and color. When a player claims a field (clicks a button on a grid), that button's BackColor is changed to that player's color.
What I'd like to do now is have the open fields in the grid become a semi-transparent shade of a player's color while the cursor is over a field.
For some reason, this isn't working for my buttons:
Public Class FieldButton
Inherits Button
' ... Omitting for brevity '
Private _mouseIn As Boolean
Protected Overrides Sub OnMouseEnter(e As EventArgs)
MyBase.OnMouseEnter(e)
_mouseIn = True
End Sub
Protected Overrides Sub OnMouseLeave(e As EventArgs)
MyBase.OnMouseLeave(e)
_mouseIn = False
End Sub
Public Overrides Property BackColor As Color
Get
If Field.HasOwner Then
Return Field.Owner.Color
ElseIf _mouseIn Then
Return Color.FromArgb(16, Presenter.Game.CurrentPlayer.Color)
End If
Return MyBase.BackColor
End Get
Set(value As Color)
MyBase.BackColor = value
End Set
End Property
Private Shared ReadOnly FullPen As New Pen(Brushes.Black, 3)
Private Shared ReadOnly SemiTransparentPen As New Pen(Color.FromArgb(64, Color.Black), 3)
Protected Overrides Sub OnPaint(pevent As PaintEventArgs)
MyBase.OnPaint(pevent)
If Field.HasOwner Then
PaintMark(pevent.Graphics, pevent.ClipRectangle, Field.Owner.Mark, FullPen)
ElseIf _mouseIn And Not Presenter.Game.IsGameOver Then
PaintMark(pevent.Graphics, pevent.ClipRectangle, Presenter.Game.CurrentPlayer.Mark, SemiTransparentPen)
End If
End Sub
' ... '
End Class
In the above code, Field is another object that represents a field in the grid. Each field has an Owner, which is set to the player that claimed the field (or null).
Anyway, the line that is supposed to do the magic:
Return Color.FromArgb(16, Presenter.Game.CurrentPlayer.Color)
Has the following result:
It might be a little difficult to see due to the illusion created by the semi-transparent mark, but the button background color FromArgb(16, ...) is exactly the same as the buttons with alpha channel 255.
What am I not doing right?
EDIT
Turns out that the button's FlatButtonAppearance.MouseOverBackColor property takes precedence over the button's BackColor when FlatStyle = FlatStyle.Flat.
I don't think that explains why my button was still showing purple on mouse-over. I'm guessing that MouseOverBackColor defaults to the current backcolor, but ignores the alpha channel.
Research says this should work for you, add it to your buttons constructor;
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true)
http://msdn.microsoft.com/en-us/library/system.windows.forms.control.setstyle.aspx
http://msdn.microsoft.com/en-us/library/system.windows.forms.controlstyles.aspx

Retrieving data on dynamic controls

I am using dynamically created controls and need to retrieve information about the control at runtime.
If IsLoaded <> "free" Then
flow_display.Controls.Clear()
For x As Integer = 0 To populate.Count - 1
If populate(x).parentID = 2 Then
Dim NewPicBox As PictureBox = New PictureBox
NewPicBox.Size = New System.Drawing.Size(697, 50)
NewPicBox.ImageLocation = pw_imgLink & populate(x).imageID
AddHandler NewPicBox.Click, AddressOf catWindow
flow_display.Controls.Add(NewPicBox)
End If
Next
IsLoaded = "free"
End If
End Sub
Here I create the control when the user clicks on the appropriate label. Right now the catWindow sub is empty. I need to figure out which button is clicked and figure out its location on the populate list. I have tried a few things and from what I've read from other questions can't seem to find anything the helps. Thanks :)
For finding out which PictureBox is pressed, your catWindow Sub should look like this:
Public Sub catWindow(ByVal sender As Object, ByVal e As EventArgs)
Dim box As PictureBox = TryCast(sender, PictureBox)
If box Is Nothing Then Exit Sub
'Now "box" refers to the PictureBox that was pressed
'...
End Sub
If you want to find it's location in the populate list, you will need to iterate through the list until you find the matching box. You could also pre-empt a property on your PictureBox that isn't doing anything else and use it to store the index. Older forms tools used to have a .Tag property especially for this kind of thing. But really, the need to do this smells like a design flaw to me.
FWIW, I'd rewrite your original sample like this:
If IsLoaded <> "free" Then
flow_display.SuspendLayout()
flow_display.Controls.Clear()
For Each box As PictureBox In populate
.Where(Function(p) p.parentID = 2)
.Select(Function(p) New PictureBox() With {
.Size = New System.Drawing.Size(697, 50),
.ImageLocation pw_imgLink & p.imageID })
AddHandler box.Click, AddressOf catWindow
flow_display.Controls.Add(box)
Next box
flow_display.ResumeLayout()
IsLoaded = "free"
End If