Quite often as I browse through the VB.NET Forums on vbCity I see questions about tasks that can be quite difficult in WinForms, but would be much easier to deal with using WPF. One recent question that fits this description is the following one:
|
"I am wondering how I can change a specific line of text in a listbox based on a condition.
For example, if I subract listbox.items.item(1) from listbox.items.item(0) and the posted result in position (2) is a negative decimal then make item(2) red.. or if the result is greater than 1000 then turn it green.
I tried some of the examples in some other posts, but they aren't what I am looking for.
Any help would be appreciated. |
As you can see, essentially this is a task where a ListBox contains some items that represent numeric values. Some simple arithmetic is used to calculate the value for the third line. The color of the text on that line is decided depending on whether the value is negative or higher than 1000.
In Windows Forms this job would need the use of OwnerDraw in the ListBox, with each line being drawn using the GDI+ DrawString method. While not that daunting a task, OwnerDraw and DrawString can get a bit unwieldy if you have a lot of data to handle. So, even though OwnerDraw would probably be fine for this particular example, I still thought it was worth seeing how WPF might be used to deal with it.
With my WPF head on, I first thought that I would tackle this with a value converter using IValueConverter, reading the values and setting the Brush color based on the value. But as it turns out, WPF's innate ability to accept varying settings for each individual ListBoxItem's Foreground property made it a much easier proposition.
The Basics
Because I'm concentrating on the UI here, not the mechanics of how to get the values, I've short-circuited this part and simply hard-coded the values into the ListBox. (Don't worry, I'll get to how you can take values from the user, insert them into the ListBox and do the arithmetic at the end, if that's something you need.)
Here's the Xaml to create the ListBox and those three items:
<Window x:Class="BasicTask"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BasicTask" Height="300" Width="300">
<Grid>
<ListBox Margin="79,38,79,65" Name="ListBox1" FontSize="20">
<ListBoxItem Name="Value1">2000</ListBoxItem>
<ListBoxItem Name="Value2">3000</ListBoxItem>
<ListBoxItem Name="Value3">-1000</ListBoxItem>
</ListBox>
</Grid>
</Window>
(I've manually done the arithmetic which is very unrealistic, but keeps the code to a minimum for now)
Of the various ways of tackling this, I decided to use a simple Select Case block in the Code-Behind. The ListBox control has a LayoutUpdated event, which fires whenever there is a change to the layout of the visual elements of the control. We can use this event to apply the appropriate colors to the text items.
Partial Public Class BasicTask
Private Sub ListBox1_LayoutUpdated(ByVal sender As Object, ByVal e As System.EventArgs) Handles ListBox1.LayoutUpdated
Select Case CDbl(Value3.Content)
Case Is < 0
Value3.Foreground = Brushes.Red
Case Is > 1000
Value3.Foreground = Brushes.DarkSeaGreen
Case Else
Value3.Foreground = Brushes.Black
End Select
End Sub
End Class
So if you already have a ListBox with values in it, that's all the code you need. It's quicker and cleaner than the OwnerDraw route, I think you'll agree.
Taking User Input
As hard-coding those values in the Xaml isn't very realistic, as promised earlier, here is one way to create the ListBox in Xaml and update it in code:
<Grid>
<Button Height="23" HorizontalAlignment="Left" Margin="22,91,0,0" Name="Button1" VerticalAlignment="Top" Width="75">Enter</Button>
<TextBox Height="23" Margin="22,34,0,0" Name="TextBox1" VerticalAlignment="Top" HorizontalAlignment="Left" Width="84">2500</TextBox>
<TextBox Height="23" Margin="0,34,31,0" Name="TextBox2" VerticalAlignment="Top" HorizontalAlignment="Right" Width="90">900</TextBox>
<Label Height="28" HorizontalAlignment="Left" Margin="22,0,0,0" Name="Label1" VerticalAlignment="Top" Width="96">First Value</Label>
<Label Height="28" HorizontalAlignment="Right" Margin="0,0,25,0" Name="Label2" VerticalAlignment="Top" Width="96">Second Value</Label>
<ListBox Margin="0,91,31,33" Name="ListBox1" FontSize="20" HorizontalAlignment="Right" Width="120">
<ListBoxItem Name="Value1">0</ListBoxItem>
<ListBoxItem Name="Value2">0</ListBoxItem>
<ListBoxItem Name="Value3">0</ListBoxItem>
</ListBox>
</Grid>
Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
Value1.Content = TextBox1.Text
Value2.Content = TextBox2.Text
Value3.Content = (CDbl(TextBox1.Text) - CDbl(TextBox2.Text)).ToString
End Sub
Private Sub ListBox1_LayoutUpdated(ByVal sender As Object, ByVal e As System.EventArgs) Handles ListBox1.LayoutUpdated
Select Case CDbl(Value3.Content)
Case Is < 0
Value3.Foreground = Brushes.Red
Case Is > 1000
Value3.Foreground = Brushes.DarkSeaGreen
Case Else
Value3.Foreground = Brushes.Black
End Select
End Sub
The result will look something like this

As you change the values in the two TextBoxes, the ListBox will update and the text color of the third item will reflect the rules in the LayoutUpdated event.
Because the scenario has a preset configuration of just those three items, the above approach is the easiest.
Varying Number of ListBoxItems
If for some reason you can't or don't want to create the three ListBoxItems in Xaml, then you can easily (if slightly more verbosely) do this in the code-behind:
Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
ListBox1.Items.Clear()
Dim lbi As New ListBoxItem
lbi.Content = TextBox1.Text
ListBox1.Items.Add(lbi)
lbi = New ListBoxItem
lbi.Content = TextBox2.Text
ListBox1.Items.Add(lbi)
lbi = New ListBoxItem
lbi.Content = (CDbl(TextBox1.Text) - CDbl(TextBox2.Text)).ToString
ListBox1.Items.Add(lbi)
Dim item3 As New ListBoxItem
item3 = CType(ListBox1.Items(2), ListBoxItem)
Select Case CDbl(item3.Content)
Case Is < 0
item3.Foreground = Brushes.Red
Case Is > 1000
item3.Foreground = Brushes.DarkSeaGreen
Case Else
item3.Foreground = Brushes.Black
End Select
End Sub
You can of course vary the number of ListBoxItems added in this way and select another of them for the text color formatting treatment. You'll see that I put all the code in the Button click this time. If you're wondering why, it's because the LayoutUpdated event fires when the ListBox is first created. As there are no ListBoxItems in existence at that moment, you will get an exception when you try and access the non-existent ListBox1.Items(2) item.
And Finally ....
Although it's not part of the original question, you might at some time want to comb through a ListBox like this and change the color of all the lines that meet our criteria of less than zero or more than 1000. Enumerating through the ListBoxItems will do this job for you:
Private Sub ListBox1_LayoutUpdated(ByVal sender As Object, ByVal e As System.EventArgs) Handles ListBox1.LayoutUpdated
For Each l As ListBoxItem In Me.ListBox1.Items
Select Case CDbl(l.Content)
Case Is < 0
l.Foreground = Brushes.Red
Case Is > 1000
l.Foreground = Brushes.DarkSeaGreen
Case Else
l.Foreground = Brushes.Black
End Select
Next
End Sub
And as a final thought, if you only want to turn text Red if the value is below zero and leave it as Black for all other values then you can replace that multi line Select Case block with a single line IIF statement.
For completeness, all the above samples should have some validation added to ensure that the items exist and that the text represents valid numeric values.