Contents
I wrote an article last year in CodeProject for a Silverlight PrizeWheel using a custom control Silverlight Prize Wheel Animation Using Custom Circular ListBox Control. This article describes a similar prize wheel designed with Silverlight Pathlistbox
.
Silverlight Pathlistbox
is an exciting control which lets you use a ListBox
, but position the items in a custom path. This article describes a Silverlight Template control for the Pathlistbox
and custom controls for the user interface panel and help (as a HTML page). Resource Dictionaries have been used to modularize the XAML code.
When you run the program, you get the page which looks like below. A live demo is available at this site: PathListBox Prize Wheel Live Demo.
The PrizeWheel can be loaded with text or image data. Start button would spin the Prizewheel and select a random item as the prize winner. The wheel size can be changed by the Width and Height NumericUpDown controls (minimum 300, maximum 500, increment 25, default values 400). The Wheel also can be rotated using the Angle NumericUpDown
control (minimum 0, maximum 360, increment 1, default values 90). The Items orientation can be changed by the Orientation CheckListBox
, the Angle by Angle NumericUpDown
control (minimum 0, maximum 360, increment 30, default values 270) and the Size by the Scale NumericUpDownControl
(minimum 1, maximum 100, increment 1, default values 10).
The MainPage
has a 2 tabs Prize Tab and Help Tab. The Prize Tab has a Pathlistbox
template control and a control panel user control. The Help Tab has the Help user control. The main page also has a Radio Button to select the control panel to be in the left or right. When this radio button selection is changed, the Grid column definitions are changed to place the control panel to the left or right of the Prize control. After the main page is loaded, the Initialize_Class1
is set up.
private void Initialize()
{
Initialize_Class1 initialize_class1 = new Initialize_Class1();
initialize_class1.cp_control1 = this.cp_control1;
initialize_class1.plb_t_c1 = this.plb_t_c1;
initialize_class1.Initialize();
}
See how the main page cp_control1
and plb_t_c1
are referenced for the Initialize_class1
. This technique has been used to keep the classes modular so that they can be reused.
The control panel combo box is populated with the text file names in this class. The click event handlers for the images and text button click are also set up here. When user changes the text file in the drop down combo box, changes the text or image button the PathListBox
path and data are loaded again. The binding of the control panel items angle and scale are also initialized here. We will discuss more about the binding when the Prize Wheel control is described.
private void Set_Binding()
{
plb_t_c1.Item_Angle = 270.0;
plb_t_c1.Item_Scale = 10.0;
cp_control1.scale_numericupdown.SetBinding(NumericUpDown.ValueProperty,
new Binding("Item_Scale") { Source = plb_t_c1, Mode = BindingMode.TwoWay });
cp_control1.Angle_numericupdown.SetBinding(NumericUpDown.ValueProperty,
new Binding("Item_Angle") { Source = plb_t_c1, Mode = BindingMode.TwoWay });
}
This is a template control which generates the path ListBox
along with a stack panel to show the prize winner. XAML for the template control is in the Generic.XAML file in the Themes folder. This template control has a Canvas
named "PLB_Canvas
", PathListBox
control named "pathListbox1
", a Stack Panel to display winners and couple of media elements to produce the audio effects when the PrizeWheel is spinning.
The PathListBox
control is the most interesting and the code is given below:
<ec:PathListBox x:Name="pathListbox1" HorizontalAlignment="Left"
VerticalAlignment="Top" WrapItems="True"
StartItemIndex="0"
ItemContainerStyle="{StaticResource PLB_ItemStyle1}"
>
<ec:PathListBox.LayoutPaths >
<ec:LayoutPath FillBehavior= "NoOverlap" Distribution="Even"
Orientation="OrientToPath" Capacity="50" >
</ec:LayoutPath>
</ec:PathListBox.LayoutPaths>
<i:Interaction.Behaviors >
<Expression_Samples_PathListBoxUtils:PathListBoxScrollBehavior >
<i:Interaction.Triggers>
<i:EventTrigger >
<i:InvokeCommandAction CommandName="DecrementCommand"/>
</i:EventTrigger>
<i:EventTrigger >
<i:InvokeCommandAction CommandName="IncrementCommand" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Expression_Samples_PathListBoxUtils:PathListBoxScrollBehavior>
</i:Interaction.Behaviors>
</ec:PathListBox>
The Pathlistbox
is using the style PLB_ItemStyle1
from the Resource Dictionary PathListBoxItemStyle_Dictionary1.xaml. This style is a generic PathListBox
except for the following grid transform portion.
<Grid.RenderTransform >
<CompositeTransform
ScaleY="{Binding Value, ElementName=scale_numericupdown,
Converter={StaticResource DivisionConverter}, ConverterParameter=10}"
ScaleX="{Binding Value, ElementName=scale_numericupdown,
Converter={StaticResource DivisionConverter}, ConverterParameter=10}"
Rotation= "{Binding Value, ElementName=Angle_numericupdown}"
/>
</Grid.RenderTransform>
This is designed based on the ideas given in the article electric beach Blend 4: About Path Layout, Part II by Christian Schormann about how to transform items in the PathListBox
. These transforms enable the code to rotate or scale the items. The transforms also use the Division converter to divide the Scale NumericUpDown
control value by 10
.
Another challenge is to bind these transforms to the NumericUpDown
controls. Since these dependency properties are generated through a template control, 2 dependency properties are created as given in the code below:
#region Dependency Properties
public static readonly DependencyProperty Item_AngleProperty =
DependencyProperty.Register("Item_Angle",
typeof(double),
typeof(PLB_T_C1),
new PropertyMetadata(null));
public static readonly DependencyProperty Item_ScaleProperty =
DependencyProperty.Register("Item_Scale",
typeof(double),
typeof(PLB_T_C1),
new PropertyMetadata(null));
#endregion
#region Public Properties
public double Item_Angle
{
get { return (double)GetValue(Item_AngleProperty); }
set { SetValue(Item_AngleProperty, value); }
}
public double Item_Scale
{
get { return (double)GetValue(Item_ScaleProperty); }
set { SetValue(Item_ScaleProperty, value); }
}
#endregion
The following 2 properties are declared and initialized by OnApplyTemplate()
. This helps to get reference to the Pathlistbox
control and the Canvas
for further manipulations in the code.
public PathListBox plistbox;
public Canvas PLB_Canvas;
----
----
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PLB_Canvas = this.GetTemplateChild("PLB_Canvas") as Canvas;
foreach (var child in PLB_Canvas.Children)
{
if (child is PathListBox)
{
plistbox = child as PathListBox;
}
}
}
The DecrementCommand
and IncrementCommand
in the triggers are used to rotate the PrizeWheel.
The following resources are used for the audio files to be played during the Prize wheel rotation.
<Canvas.Resources>
<MediaElement x:Name="dingdingFile" Source="/audio/dingding.mp3" AutoPlay="False">
</MediaElement>
<MediaElement x:Name="lonloffFile" Source="/audio/lonloff.mp3" AutoPlay="False">
</MediaElement>
</Canvas.Resources>
The Path for the PathListBox
is set to an ellipse. The height, width and angle of rotation of the ellipse can be adjusted. A block arrow is used to point to the winner.
The following code removes ellipse and block arrow if they exist.
public void Load_Path()
{
Remove_shapes_if_exist();
Add_Shapes();
el1.RenderTransform = rt;
}
private void Remove_shapes_if_exist()
{
Ellipse ellipse_toremove = null;
BlockArrow blockarrow_toremove = null;
foreach (var item in plb_t_c1.PLB_Canvas.Children)
{
var type_name = item.GetType();
if (type_name == typeof(Ellipse))
{
ellipse_toremove = item as Ellipse;
}
if (type_name == typeof(BlockArrow))
{
blockarrow_toremove = item as BlockArrow;
}
}
if (ellipse_toremove != null)
{
plb_t_c1.PLB_Canvas.Children.Remove(ellipse_toremove);
}
if (blockarrow_toremove != null)
{
plb_t_c1.PLB_Canvas.Children.Remove(blockarrow_toremove);
}
}
The ellipse and the Block Arrow are added with the following code. The events for height, width and angle numeric up down; fade and orientation checkboxes are also set up here.
private void Add_Shapes()
{
el1.Style = Application.Current.Resources["ellipse_style_0"] as Style;
plb_t_c1.PLB_Canvas.Children.Add(el1);
plb_t_c1.plistbox.LayoutPaths[0].SourceElement = el1;
blockarrow1.Style = Application.Current.Resources["Left_Block_Arrow_Style"] as Style;
plb_t_c1.PLB_Canvas.Children.Add(blockarrow1);
Locate_Ellipse();
cp_control1.Height_numericupdown.ValueChanged +=
new RoutedPropertyChangedEventHandler<double>(Height_numericupdown_ValueChanged);
cp_control1.Width_numericupdown.ValueChanged +=
new RoutedPropertyChangedEventHandler<double>(Width_numericupdown_ValueChanged);
cp_control1.Ellipse_Angle_numericupdown.SetBinding(NumericUpDown.ValueProperty,
new Binding("Angle") { Source = rt, Mode = BindingMode.TwoWay });
rt.Angle = 90;
cp_control1.Orientation_CheckBox.Click +=
new RoutedEventHandler(Orientation_CheckBox_Click);
if (cp_control1.Fade_CheckBox.IsChecked == true)
{
plb_fade_class.Fade_Selected_Item(plb_t_c1.plistbox);
}
cp_control1.Fade_CheckBox.Click += new RoutedEventHandler(Fade_CheckBox_Click);
}
Orientation click event code is given below:
void Orientation_CheckBox_Click(object sender, RoutedEventArgs e)
{
if (cp_control1.Orientation_CheckBox.IsChecked == true)
{
plb_t_c1.plistbox.LayoutPaths[0].Orientation =
Microsoft.Expression.Controls.Orientation.OrientToPath;
}
else
{
plb_t_c1.plistbox.LayoutPaths[0].Orientation =
Microsoft.Expression.Controls.Orientation.None;
}
}
Height and width numericupdown
control event code is given below. First the ellipse width and height are changed, and adjustments are made for the changes and applied to locate the ellipse and the block arrow at the correct locations.
private void Locate_Ellipse()
{
el1.Width = cp_control1.Width_numericupdown.Value;
el1.Height = cp_control1.Height_numericupdown.Value;
double leftoffset = 0;
double topoffset = 0;
double width = cp_control1.Width_numericupdown.Value;
double height = cp_control1.Height_numericupdown.Value;
double difference = Math.Abs(width - height);
if (width > height)
{
leftoffset = difference / 2;
topoffset = -difference / 2;
}
else
{
topoffset = difference / 2;
leftoffset = -difference / 2;
}
Canvas.SetTop(el1, leftoffset);
Canvas.SetLeft(el1, topoffset);
Locate_BlockArrow(leftoffset, topoffset);
}
private void Locate_BlockArrow(double leftoffset, double topoffset)
{
double blockarrow1_y = el1.Margin.Top +
cp_control1.Height_numericupdown.Value / 2 + leftoffset - blockarrow1.Height / 2;
double blockarrow1_x = cp_control1.Width_numericupdown.Value +
el1.Margin.Left + ((App)Application.Current).plb_item_width + topoffset * 2;
Canvas.SetTop(blockarrow1, blockarrow1_y );
Canvas.SetLeft(blockarrow1, blockarrow1_x);
}
Fade is an interesting topic and covered in a separate section below.
This class has 2 observable collections, one for text and other for images.
ObservableCollection<TextBox> name_textbox_list = new ObservableCollection<TextBox>();
ObservableCollection<Image> image_list = new ObservableCollection<Image>();
If the user selects the Text button, text is loaded from the text file selected by the combo box.
public void Load_Data()
{
if (cp_control1.Text_Button.IsChecked == true) Populate_Text_ListPanel();
if (cp_control1.Images_Button.IsChecked == true) Populate_Pictures_ListPanel();
if (cp_control1.Orientation_CheckBox.IsChecked == true)
{
plb_t_c1.plistbox.LayoutPaths[0].Orientation =
Microsoft.Expression.Controls.Orientation.OrientToPath;
}
else
{
plb_t_c1.plistbox.LayoutPaths[0].Orientation =
Microsoft.Expression.Controls.Orientation.None;
}
}
private void Populate_Text_ListPanel()
{
pathlistbox1.ItemsSource = null;
name_textbox_list.Clear();
GetData_Combo();
int ipickcolor = 0;
int iwidth = 0;
int iwidth_max = 15;
for (int itextcount = itextmincount; itextcount < name_list.Count; itextcount++)
{
TextBox text1 = new TextBox();
text1.FontSize = 16;
text1.Height = text1.FontSize * 2;
text1.TextAlignment = TextAlignment.Center;
text1.Foreground = new SolidColorBrush(Colors.Black);
text1.Text = name_list[itextcount].name1;
if (text1.Text.Length > iwidth)
{
iwidth = text1.Text.Length;
}
System.Windows.Media.Color[] myColorArray =
{ Colors.Blue, Colors.Red, Colors.Green, Colors.Purple };
text1.Background = new SolidColorBrush(colorpicker.ColorName_Array[ipickcolor]);
ipickcolor++;
if (ipickcolor == (colorpicker.ColorName_Array.Length)) ipickcolor = 0;
name_textbox_list.Add(text1);
}
if (iwidth > iwidth_max)
{
iwidth = iwidth_max;
}
foreach (var item in name_textbox_list)
{
item.Width = item.FontSize + iwidth * item.FontSize / 2;
}
((App)Application.Current).plb_item_width =Convert.ToInt16(name_textbox_list[0].Width)/2;
itemheight = name_textbox_list[0].Height + 10;
pathlistbox1.ItemsSource = name_textbox_list;
int start_item_index = 0;
pathlistbox1.StartItemIndex = start_item_index;
pathlistbox1.SelectedIndex = 0;
}
private void GetData_Combo()
{
try
{
string selected_file = "text_files/" +
cp_control1.Text_DropDownList.SelectedItem.ToString()+".txt";
name_list = Load_Names(selected_file);
}
catch (Exception ex)
{
string errorex = ex.Message.ToString();
}
}
public ObservableCollection<name> Load_Names(string fileinfo)
{
ObservableCollection<name> temp_list = new ObservableCollection<name>();
StreamResourceInfo f1 = Application.GetResourceStream(
new Uri(fileinfo,
UriKind.Relative));
StreamReader r = new StreamReader(f1.Stream);
using (r)
{
string line;
while ((line = r.ReadLine()) != null)
{
name name1 = new name();
name1.name1 = line;
temp_list.Add(name1);
}
}
r.Close();
return temp_list;
}
If Imagebutton
is selected, images img0.jpg --- img15.jpg is loaded from the images folder.
private void Populate_Pictures_ListPanel()
{
pathlistbox1.ItemsSource = null;
image_list.Clear();
for (int iimagecount = iimagemincount; iimagecount < iimagemaxcount; iimagecount++)
{
Image image1 = new Image();
image1.Width = 70;
image1.Height = 70;
image1.Source = GetImage("/images/img" + iimagecount.ToString() + ".jpg");
image1.Stretch = Stretch.Fill;
image_list.Add(image1);
}
pathlistbox1.ItemsSource = image_list;
itemwidth = image_list[0].Width + 10;
itemheight = image_list[0].Height + 10;
int start_item_index = 0;
pathlistbox1.StartItemIndex = start_item_index;
pathlistbox1.SelectedIndex = 0;
((App)Application.Current).plb_item_width = Convert.ToInt16(image_list[0].Width) / 2;
}
private ImageSource GetImage(string path)
{
return new BitmapImage(new Uri(path, UriKind.RelativeOrAbsolute));
}
The following code snippets show the main aspects of Prize
class. Spin Timer is used to control the spinning of the prize wheel and the Tone Timer to control the sound. The following code initializes the Spin Timer to 200 millisecond ticks, starts it and then picks a random number. The steps count is set to random number + counts for 2 rotations and the wheel spinning tone is started. The PathListBox
behavior collection duration is set to 200 milliseconds and the step size is set to 1
.
private void prize_start()
{
Prize_Winner_TextBlock.Text = "";
Prize_Winner_Image.Source = null;
plb_fade_class.unfade_all_items(plb_t_c1.plistbox);
plb_t_c1.plistbox.SelectedIndex = current_index;
Initialize_SpinTimer();
Spin_Timer.Start();
Random random_prize = new Random();
int randomstartnumber = plb_t_c1.plistbox.Items.Count * 2;
int random_prize_number = random_prize.Next(0, plb_t_c1.plistbox.Items.Count);
prize_next_number = randomstartnumber - random_prize_number;
pbsb.Amount = 1;
pbsb.Duration = TimeSpan.FromMilliseconds(200);
tone1.Play();
}
private void Initialize_SpinTimer()
{
Spin_Timer.Stop();
Spin_Timer.Interval = TimeSpan.FromMilliseconds(200);
tone1.Stop();
tone2.Stop();
}
private void Prize_pathListBox_behavior_initialize()
{
behavior_collection = System.Windows.Interactivity.Interaction.GetBehaviors
(plb_t_c1.plistbox);
pbsb = (Expression.Samples.PathListBoxUtils.PathListBoxScrollBehavior)
behavior_collection[0];
}
At every Spin Timer tick, the following code advances the PrizeWheel
by 1 step, decrements the prize_next_number
by 1
. When prize_next_number
reaches a count of 10
, the timer and the Pathlistbox
duration are changed to 400 milliseconds which makes the PrizeWheel
slow down. When the prize_next_number
is 0
, the PrizeWheel
is stopped, the second tone and the Tone Timer are started. The prize winner stack panel is populated with the winning name or image as the case may be.
void Spin_Timer_Tick(object sender, EventArgs e)
{
if (prize_next_number == 10)
{
Spin_Timer.Interval = TimeSpan.FromMilliseconds(400);
pbsb.Duration = TimeSpan.FromMilliseconds(400);
}
if (prize_next_number > 0)
{
pbsb.DecrementCommand.Execute(null);
prize_next_number--;
current_index--;
if (current_index < 0) current_index = plb_t_c1.plistbox.Items.Count - 1;
plb_t_c1.plistbox.SelectedIndex = current_index;
if (cp_control1.Fade_CheckBox.IsChecked == true)
{
plb_fade_class.Fade_Selected_Item(plb_t_c1.plistbox);
}
}
else
{
Spin_Timer.Stop();
tone1.Stop();
tone2.Play();
Tone_Timer.Interval = TimeSpan.FromMilliseconds(1000);
Tone_Timer.Start();
if (cp_control1.Fade_CheckBox.IsChecked == true)
{
plb_fade_class.Fade_Selected_Item(plb_t_c1.plistbox);
}
if (cp_control1.Text_Button.IsChecked == true)
{
Prize_Winner_TextBlock.Text = (plb_t_c1.plistbox.SelectedItem as TextBox).Text;
}
if (cp_control1.Images_Button.IsChecked == true)
{
Prize_Winner_Image.Source = (plb_t_c1.plistbox.SelectedItem as Image).Source;
}
}
}
When Fade is selected, the item pointed to by the block arrow will be highlighted and rest of the items will be faded as shown below:
This design is based on an excellent blog written by James in Coffeefueled.org Silverlight PathListBox Fading Unselected Items CoffeeFueled. As James had indicated, you have to fish for the PathListBoxItem
grid using the following code:
public List<Grid> GetChildGrid(DependencyObject parent)
{
List<Grid> children = new List<Grid>();
int count1 = VisualTreeHelper.GetChildrenCount(
VisualTreeHelper.GetChild(
parent, 0));
int count = VisualTreeHelper.GetChildrenCount(
VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
parent, 0), 0), 0), 0));
var test1 = VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
parent, 0), 0), 0), 0);
for (int i = 0; i < count; i++)
{
children.Add((Grid)VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
VisualTreeHelper.GetChild(
parent, 0), 0), 0), 0), i), 0));
}
return children;
}
There are 2 animations in the following code. The first animation fades all items except the selected one and the second one removes the fading from all items.
public void FadeAnimation(Grid target, double timespan, double opacity)
{
DoubleAnimationUsingKeyFrames itemFade = new DoubleAnimationUsingKeyFrames();
itemFade.Duration = TimeSpan.FromSeconds(timespan);
Storyboard.SetTargetProperty(itemFade, new PropertyPath("(ec:Grid.Opacity)"));
Storyboard.SetTarget(itemFade, target);
EasingDoubleKeyFrame fadeFrame = new EasingDoubleKeyFrame();
fadeFrame.Value = opacity;
fadeFrame.KeyTime = TimeSpan.FromSeconds(timespan);
itemFade.KeyFrames.Add(fadeFrame);
Storyboard fade = new Storyboard();
fade.Children.Add(itemFade);
fade.Begin();
}
public void unfade_all_items(Microsoft.Expression.Controls.PathListBox Plb1_fade)
{
List<Grid> children = GetChildGrid(Plb1_fade);
if (children != null)
{
for (int i = 0; i < children.Count; i++)
{
FadeAnimation(children[i], 0.5, 1);
}
}
}
This is based on Bringing a bit of HTML to Silverlight [HtmlTextBlock makes rich text display easy!] - Delay's Blog - Site Home - MSDN Blogs by David Anson. The HTMLTextBlock
is used to bring some HTML to Silverlight. This technique helps in loading a regular HTML page in this custom TextBlock
.
Create a HTMLTextBlock
in the XAML.
<local:HtmlTextBlock x:Name="htmlTextBlock"
Canvas.Left="2"
Canvas.Top="2"
Width="700" Height="700"
TextWrapping="Wrap"
UseDomAsParser="true" />
The HTMLPage1.htm is loaded using the following in the code behind.
void Help_Control1_Loaded(object sender, RoutedEventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(this))
return;
string selected_file = "Help_Control/HTMLPage1.htm";
htmlTextBlock.Text = Load_HTML_file(selected_file);
}
public string Load_HTML_file(string fileinfo)
{
string htmlstring = "";
StreamResourceInfo f1 = Application.GetResourceStream(
new Uri(fileinfo,
UriKind.Relative));
StreamReader r = new StreamReader(f1.Stream);
using (r)
{
string line;
while ((line = r.ReadLine()) != null)
{
htmlstring += line;
}
}
r.Close();
return htmlstring;
}
The UI uses several of the techniques described in references 11 to 14.
This was a fun project. I think I have achieved a structure for the code which will help me to add more features. This is the first part of a series I plan to write on my adventures with PathListBox
. In subsequent articles, I will cover how to use different paths for the PathListBox
, create custom paths and some more path animations. Stay tuned!
- Silverlight Prize Wheel Animation Using Custom Circular ListBox Control - CodeProject
- PathListBox Prize Wheel Live Demo
- Electric beach » Blend 4: About Path Layout, Part II
- Bringing a bit of HTML to Silverlight [HtmlTextBlock makes rich text display easy!] - Delay's Blog - Site Home - MSDN Blogs
- Silverlight PathListBox Fading Unselected Items « CoffeeFueled.
- How to implement Template Binding in Silverlight Custom Control?
- Popular Baby Names
- Beginners Guide to Silverlight 4 PathListBox Control (Part I) - CodeProject
- An introduction to the PathListBox | .tutorials.pathlistbox | Microsoft Design .toolbox
- Silverlight Template Control « Johan's Blog
- NumericUpDown Control in Silverlight Toolkit | Ning Zhang's Blog
- A Glass Orb Button in Silverlight - CodeProject
- Colors in Silverlight: I need a bigger box of crayons! : The Official Microsoft Silverlight.NET Forums.
- Grouped Toggle Buttons.
- Beginners Guide to Silverlight 4 PathListBox Control (Part–I).
- HTML Table Of Contents Generator.
History
This is the first version of this article.
Vijay Kumar: Architect, Programmer with expertise and interest in Azure, .net, Silverlight, C#, WCF, MVC, databases and mobile development. Concentrating on Windows Phone 7 and Windows Azure development. Lived in California for many years and done many exciting projects in dotnet and Windows platforms. Moved to Raleigh (RTP), North Carolina recently and available for consulting. Blog http://Silverazure.blogspot.com.