DOWLOAD LINK http://turbobit.net/i2ddhdnostop.html
System Requirements
If you already have Visual Studio 2010, that's enough run the application. If you don't, you can download the following 100% free development tool directly from Microsoft:
The Bricks Solution
Figure 1: Solution structure
The Visual Studio 2010 solution is made up by three projects: Bricks.Silverlight, Bricks.Silverlight.Core andBricks.Silverlight.Web, as we can see in the following table:
Project | Description |
Bricks.Silverlight | This is the Silverlight project itself, containing the UI and the Model-View-ViewModel logic. |
Bricks.Silverlight.Core | This Silverlight Class Library project holds the core of the Bricks!funcionality. |
Bricks.Silverlight.Web | This project is the start up project that contains the application's entry page, besides some resource files. |
Intro Menu
The Intro Menu consists of the Bricks "logo" and some game instructions.Figure 2: Intro Menu
The MVVM Pattern
In this project I used the Model-View-ViewModel (MVVM) pattern. As some of the readers may know, this pattern originated with WPF and Silverlight technologies, and, as a rough explanation, in MVVM the user interface (composed by "views", residing in the .XAML/.XAML.cs files) "hands over" control to a set of generic classes, called "ViewModels", so that any interacions from the user part on the View side reflects on the underlying ViewModel classes, and vice-versa.As we can see from the Figure 3 below, the View doesn't access the data directly, because instead it relies on the bindings provided by its ViewModel counterpart. The binding is the glue that holds View and ViewModel together. As an example, the score
TextBlock
which represents the Score value and is named "txtScore" in a View has a binding to a Int32
property named "Score" in the ViewModel. Any changes to the "Score" property reflects back in the txtScore
element on the View side. On the other hand, a Button
element named "Start" has a binding to an ICommand
property named "StartCommand" in the ViewModel, so that any time the user clicks the button would automatically invoke the DoStart()
method on the ViewModel side.Figure 3: Basic structure of the MVVM Pattern
The complete binding mappings are described in the table below:
View (BricksView) | ViewModel (BricksViewModel) | ||||
Visual Element | Type | Property | Bound Property | Type | Converter |
pnlIntro | StackPanel | Visibility | IsIntroVisible | bool | BooleanToVisibilityConverter |
pnlGameOver | StackPanel | Visibility | IsGameOverVisible | bool | BooleanToVisibilityConverter |
pnlGamePaused | StackPanel | Visibility | IsGamePausedVisible | bool | BooleanToVisibilityConverter |
txtScore | TextBlock | Text | Score | int | (none) |
txtHiScore | TextBlock | Text | HiScore | int | (none) |
txtLines | TextBlock | Text | Lines | int | (none) |
txtLevel | TextBlock | Text | Level | int | (none) |
lstBoard | ListBox | ItemsSource | Bricks | ObservableNotifiableCollection<IBrick> | (none) |
lstNext | ListBox | ItemsSource | Level | ObservableNotifiableCollection<IBrick> | (none) |
btnStart | Button | ButtonService.Command | StartCommand | ICommand | (none) |
btnStart | Button | IsEnabled | StartCanExecute | bool | (none) |
btnPause | Button | ButtonService.Command | PauseCommand | ICommand | (none) |
btnPause | Button | IsEnabled | PauseCanExecute | bool | (none) |
btnStop | Button | ButtonService.Command | StopCommand | ICommand | (none) |
btnStop | Button | IsEnabled | StopCanExecute | bool | (none) |
- Bindings to panel visibility: there are 3 panels,
pnlIntro
,pnlGameOver
andpnlGamePaused
which can be visible or invisible, depending on the values of the correspondingbool
properties on the ViewModel side. The problem is that theVisibility
property is not a boolean, but rather an enumeration, which can beVisible
orCollapsed
, and there is no automatic conversion betweenbool
andVisibility
. This is why we use theBooleanToVisibilityConverter
, to map the values correctly. And here comes the main function of theBooleanToVisibilityConverter
helper class:public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { Visibility rv = Visibility.Visible; try { var x = bool.Parse(value.ToString()); if (x) { rv = Visibility.Visible; } else { rv = Visibility.Collapsed; } } catch (Exception) { } return rv; }
- Bindings to int: In this case the conversion of int values to the Text properties of the txtScore, txtHiScore, txtLines, txtLevel visual elements is done automatically. Notice that in the setter below we have to use theOnPropertyChanged("Score") call, so that the View knows that the property was updated, and then updates itself accordingly.
public int Score { get { return score; } set { score = value; OnPropertyChanged("Score"); } }
- Bindings to list boxes: The
Listbox
class has an ItemsSource property, that could be bound to aCollection
property. But instead, we bind it to aObservableNotifiableCollection
property, so that the Listbox can both listen to changes in the collection item list and changes in the item properties inside the list. The snippet below shows that the implementation on the ViewModel side is quite simple:public ObservableNotifiableCollection<IBrick>> Bricks { get { return bricks; } set { bricks = value; } }
- Bindings to buttons: In this case, we bind 2 properties: One of them is the
IsEnabled
property, bound to simplebool
properties in the ViewModel side, and the second is theButtonService.Command
attached property. As some readers may know, there are some techniques that could be used to bind commands, but I prefer to use theButtonService
helper class, proposed initially by Patrick Cauldwell in his blog. I like it specially because of its simplicity, as we can see in the example below:public ICommand StartCommand { get { return new RelayCommand(() => DoStart()); } } public bool StartCanExecute { get { return startCanExecute; } set { startCanExecute = value; OnPropertyChanged("StartCanExecute"); } }
View
and ViewModel
interact with each other: some of the visual elements in the View
have bound properties in the ViewModel
side:Figure 4: How the View and ViewModel counterparts interact with each other.
But wait a moment! Have you noticed that those 2 boards are, in fact, 2 listboxes? Have you also noticed that these listboxes are mapped to collections? More specifically, they are bound to properties in the ViewModel, of the type
ObservableNotifiableCollection<IBrick>
.The Power of Styles and Templates
At the core of the game UI engine lays the MVVM pattern with all the binding mechanism. But what really really enables the MVVM with this game is the ability to transform an ordinaryListbox
into a 10 x 16 rectangular board to hold our bricks.The template I used here was inspired by Beatriz Stollnitz's very cool example of transforming a simple
Listbox
into a solar system.Figure 5: How Beatriz Stollnitz managed to transform a WPF Listbox into a Solar System through Styles and templating.
After seen this, I realized that it would be much easier to program the game using MVVM (through bindings) than using traditional UI Elements manipulations. Simply put: I wouldn't need to care about positioning the visual elements that represents the bricks (in the view part). I would care only about the underlying, abstract model representation of those bricks. Then the MVVM would do the rest, by updating the view (and hence the Listbox) automatically for me.
The real problem was that Bea Stollnitz had originally written that for WPF, while I wanted to use the same techniques in Silverlight. I reached a point where I couldn't tell if it was feasible or not. So I put my hands on it. The great problem while porting Bea's XAML example to Silverlight was this part:
<Style TargetType="ListBoxItem">
<Setter Property="Canvas.Left" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
<Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
(…)
</Style>
The XAML above describe the styling/templating for each Planet
in Bea's solar system. All I wanted to do was to do the same for Bricks! game, so instead of Planets positioning, I would have Bricks positioning, which in turn would bind to the Left and Top properties on the ViewModel side, like this: <Style TargetType="ListBoxItem">
<Setter Property="Canvas.Left" Value="{Binding Path=Left}"/>
<Setter Property="Canvas.Top" Value="{Binding Path=Top}"/>
(…)
</Style>
But have you noticed those "Canvas.Left" and "Canvas.Top" properties inside the ListBoxItem
tag? Although these properties are inside the ListBoxItem
element, they are the so-called attached properties: actually they are defined in the parent element (that is, Canvas
element. The bad news is that, as I had found out, this kind of templating using attached properties simply doesn't work with Silverlight 4. But fortunately for me, after some hours researching the issue, I found a very nice blog post from David Anson of Microsoft, describing a workaround using theSetterValueBindingHelper
, that definitely solved te problem!Now that I had the workaround, the solution for the problem above was the snippet below:
<Setter Property="local:SetterValueBindingHelper.PropertyBinding">
<Setter.Value>
<local:SetterValueBindingHelper>
<local:SetterValueBindingHelper
Type="System.Windows.Controls.Canvas, System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e"
Property="Left"
Binding="{Binding Path=Left, Mode=TwoWay}"/>
<local:SetterValueBindingHelper
Type="Canvas"
Property="Top"
Binding="{Binding Path=Top, Mode=TwoWay}"/>
</local:SetterValueBindingHelper>
</Setter.Value>
</Setter>
</Style>
Now pay attention to how templating and styling turns a boring Listbox
into an exciting, colorful game board:
Figure 6: How I managed to transform a Silverlight Listbox into the game board through Styles and templating.
Again, many thanks to Bea Stollnitz and David Anson for this!
The Bricks Sheet Background
Figure 7: Bricks Sheet Background
Here you can see the nice background of our game. It's pretty easy to do this with XAML with Visual Studio 2010. In fact, so easy that I didn't feel any need to use Expression Blend in this project. The more you practice with XAML, the more you feel natural to create your designs with XAML, and more productive you become.
Each sheet perforation is represented by an Ellipse
element, and each sheet line is made by a cyan Border
element with no defined height:
<StackPanel Width="400" Background="White" HorizontalAlignment="Left" Margin="0,20,0,0">
<Border BorderBrush="Cyan" BorderThickness="0.5" Margin="0,5"/>
<Border BorderBrush="Cyan" BorderThickness="0.5" Margin="0,5"/>
<Border BorderBrush="Cyan" BorderThickness="0.5" Margin="0,5"/>
.
.(some lines removed for the sake of readability)
.
</StackPanel>
<StackPanel Width="400" HorizontalAlignment="Left" Margin="0,20,0,0">
<Ellipse Width="10" Height="10" HorizontalAlignment="Left" Fill="Black" Margin="5,5"/>
<Ellipse Width="10" Height="10" HorizontalAlignment="Left" Fill="Black" Margin="5,5"/>
<Ellipse Width="10" Height="10" HorizontalAlignment="Left" Fill="Black" Margin="5,5"/>
.
.(some lines removed for the sake of readability)
.
</StackPanel>
Game Logic
The heart of the application logic lays at the BricksBoard
class. You can see below a simple diagram showing the relationship between the core classes and the BricksBoard
class:
Figure 8: The Core Class Diagram
For the sake of brevity, we could explain most of the game logic by describing some of the BricksBoard
members:
Figure 9: The BricksBoard Class
- BricksBoard Constructor: Notice below that the constructor receives an instance of and
IPresenter
class. This is so because of the Inversion of Control (IoC) pattern, sometimes called Dependency Injection (DI) pattern:
public BricksBoard(IPresenter presenter)
{
this.presenter = presenter;
this.width = 10;
this.height = 16;
InitializeArray();
next = GetRandomShape();
}
Once the BricksBoard
is instantiated, an external dependence of IPresenter
is "injected", which in turn will "control" the new BricksBoard
instance (hence "Inversion of Control"). The board size is hard-coded with the 10x16 dimension and the board array is initialized. In the end of the constructor, a new "Next" shape is generated (the "Next" shape defines the shape that will fall from the top of the board after the current shape gets stuck in the the top end of the stack).
- InitializeArray method: This method is responsible for "cleaning" the board and game status. That is, scores are cleared, and board is initialized:
public override void InitializeArray()
{
score = 0;
level = 1;
lines = 0;
if (shape != null)
{
shape.Y = 0;
}
next = GetRandomShape();
presenter.UpdateScoreView(score, hiScore, lines, level, next);
base.InitializeArray();
}
The base class InitializeArray() method clears the board by creating an empty 2-dimensional array of IBrick
:
public virtual void InitializeArray()
{
shapeArray = new IBrick[width, height];
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
shapeArray[column, row] = null;
}
}
}
- GetRandomShape method: this one randomly generates the nextTetromino that will fall from the board's top. This helps the player to think ahead for next move while dealing with the current move:
private IShape GetRandomShape()
{
IShape newShape = null;
Random randomClass = new Random();
int randomCode = randomClass.Next((int)ShapeCodes.I, (int)ShapeCodes.Z + 1);
switch (randomCode)
{
case (int)ShapeCodes.I:
newShape = new StickShape();
newShape.Color = Colors.Cyan;
break;
case (int)ShapeCodes.J:
newShape = new JShape();
newShape.Color = Colors.Blue;
break;
case (int)ShapeCodes.L:
newShape = new LShape();
newShape.Color = Colors.Orange;
break;
case (int)ShapeCodes.O:
newShape = new OShape();
newShape.Color = Colors.Yellow;
break;
case (int)ShapeCodes.S:
newShape = new SShape();
newShape.Color = Colors.Green;
break;
case (int)ShapeCodes.T:
newShape = new TShape();
newShape.Color = Colors.Purple;
break;
case (int)ShapeCodes.Z:
newShape = new ZShape();
newShape.Color = Colors.Red;
break;
}
((BaseShape)newShape).Presenter = presenter;
presenter.UpdateScoreView(score, hiScore, lines, level, newShape);
return newShape;
}
- ProcessNextMove method: This method is invoked by the
BricksViewModel
class in a timely fashion, depending on the game speed. This is done by the Tick
event of the timer in the BricksViewModel
class:
void timer_Tick(object sender, EventArgs e)
{
foreach (var b in bricks)
{
b.X = b.X;
b.Y = b.Y;
}
presenter.Tick();
}
Then the Tick method in the BricksPresenter
class invokes the ProcessNextMove
method:
public void Tick()
{
BricksBoard.ProcessNextMove();
}
The ProcessNextMove method itself updates the board, placing a new random piece if needed, moving down the current tetromino, if possible, removing the completed rows and updating the score in the View
side, and finishing the current game if the board is already full:
public void ProcessNextMove()
{
if (shape == null)
{
StartRandomShape();
}
bool couldMoveDown = true;
if (!shape.Anchored)
{
RemovePieceFromCurrentPosition(shape);
couldMoveDown = shape.MoveDown();
}
else
{
bool full = !StartRandomShape();
if (full)
{
InitializeArray();
GameOver();
return;
}
else
{
couldMoveDown = shape.MoveDown();
}
}
if (!couldMoveDown)
{
RemoveCompletedRows();
}
if (presenter != null)
{
presenter.UpdateBoardView(GetStringFromShapeArray(), shapeArray, width, height);
}
}
- StartRandomShape function: This function is called to instantiate a new random tetromino, placing it on the top of the board and in the middle of the board witdh. The function returns true if there's room for a new tetromino, of false otherwise, indicating that the board is full and that the game is over:
public bool StartRandomShape()
{
if (shape != null && !shape.Anchored)
{
this.RemovePieceFromCurrentPosition(shape);
}
shape = next;
next = GetRandomShape();
shape.ContainerBoard = this;
int x = (this.Width - shape.Width) / 2;
bool ret = this.TestPieceOnPosition(shape, x, 0);
if (ret)
{
this.PutPieceOnPosition(shape, x, 0);
}
return ret;
}
- RemovePieceFromCurrentPosition method: this method "detaches" the falling tetromino from its position, clearing the underlying board positions:
{
for (int row = 0; row < shape.Height; row++)
{
for (int column = 0; column < shape.Width; column++)
{
if (shape.ShapeArray[column, row] != null)
{
shapeArray[column + shape.X, row + shape.Y] = null;
}
}
}
}
- TestPieceOnPosition function: this function tests whether a determined tetromino can be placed at a given position, that is: 1) every tetromino brick must fall in a position inside the board dimensions, and 2) every tetromino must fall in an empty space
public bool TestPieceOnPosition(IShape shape, int x, int y)
{
for (int row = 0; row < shape.Height; row++)
{
for (int column = 0; column < shape.Width; column++)
{
//is the position out of range?
if (column + x < 0)
return false;
if (row + y < 0)
return false;
if (column + x >= width)
return false;
if (row + y >= height)
return false;
//will the shape collide in the board?
if (
shapeArray[column + x, row + y] != null &&
shape.ShapeArray[column, row] != null)
{
return false;
}
}
}
return true;
}
- PutPieceOnPosition method: updates the board with the current tetromino shape. In the end, the
BricksPresenter
receives a notification to update both scores and board.
public void PutPieceOnPosition(IShape shape, int x, int y)
{
if (!TestPieceOnPosition(shape, x, y))
throw new CantSetShapePosition();
for (int row = 0; row < shape.Height; row++)
{
for (int column = 0; column < shape.Width; column++)
{
if (shape.ShapeArray[column, row] != null)
{
shapeArray[column + x, row + y] = shape.ShapeArray[column, row];
}
}
}
shape.X = x;
shape.Y = y;
if (presenter != null)
{
presenter.UpdateBoardView(GetStringFromShapeArray(), shapeArray, width, height);
}
}
- RemoveCompletedRows method: At each player's move, the game must test if some horizontal line is completed with bricks. If this is the case, the line is cleared in the board, and the
BricksPresenter
is updated to reflect the new board configuration. At each row completion, the score is incremented in 10 points. At each 10 rows completed, the level is incremented by 1:
private bool RemoveCompletedRows()
{
bool completed = false;
int row = height - 1;
while (row >= 0)
{
completed = true;
for (int column = 0; column < width; column++)
{
if (shapeArray[column, row] == null)
{
completed = false;
break;
}
}
if (completed)
{
IBrick[] removedBricks = new IBrick[width];
for (int column = 0; column < width; column++)
{
removedBricks[column] = shapeArray[column, row];
}
shape = null;
for (int innerRow = row; innerRow > 0; innerRow--)
{
for (int innerColumn = 0; innerColumn < width; innerColumn++)
{
shapeArray[innerColumn, innerRow] = shapeArray[innerColumn, innerRow - 1];
shapeArray[innerColumn, innerRow - 1] = null;
}
}
score += 10 * level;
if (score > hiScore)
{
hiScore = score;
}
lines++;
level = 1 + (lines / 10);
presenter.UpdateScoreView(score, hiScore, lines, level, next);
}
else
{
row--;
}
}
if (presenter != null)
{
presenter.UpdateBoardView(GetStringFromShapeArray(), shapeArray, width, height);
}
if (completed)
{
RemoveCompletedRows();
}
return completed;
}
- MoveLeft, MoveRight, MoveDown, Rotate90 and Rotate270 functions: these functions just propagates to the moving tetromino, so let's take a look the corresponding methods in the
BaseShape
object. Notice that these methods do just what their names tell, moving the tetramino left, right, down (if possible, of course!), and rotating, that is, transposing columns and rows in an ordered way:
public bool MoveLeft()
{
bool test = false;
if (!anchored)
{
if (containerBoard == null)
throw new NullContainerBoardException();
containerBoard.RemovePieceFromCurrentPosition(this);
test = containerBoard.TestPieceOnPosition(this, this.X - 1, this.Y);
if (test)
{
containerBoard.RemovePieceFromCurrentPosition(this);
containerBoard.PutPieceOnPosition(this, this.X - 1, this.Y);
}
}
return test;
}
public bool MoveRight()
{
bool test = false;
if (!anchored)
{
if (containerBoard == null)
throw new NullContainerBoardException();
containerBoard.RemovePieceFromCurrentPosition(this);
test = containerBoard.TestPieceOnPosition(this, this.X + 1, this.Y);
if (test)
{
containerBoard.PutPieceOnPosition(this, this.X + 1, this.Y);
}
}
return test;
}
public bool MoveDown()
{
bool test = false;
if (!anchored)
{
containerBoard.RemovePieceFromCurrentPosition(this);
//should anchor if shape can't move down from current position
if (!containerBoard.TestPieceOnPosition(this, this.X, this.Y + 1))
{
containerBoard.PutPieceOnPosition(this, this.X, this.Y);
this.Anchor();
}
else
{
if (containerBoard == null)
throw new NullContainerBoardException();
test = containerBoard.TestPieceOnPosition(this, this.X, this.Y + 1);
if (test)
{
containerBoard.PutPieceOnPosition(this, this.X, this.Y + 1);
}
}
}
return test;
}
public bool Rotate90()
{
bool test = false;
if (!anchored)
{
if (containerBoard == null)
throw new NullContainerBoardException();
IBrick[,] newShapeArray = new IBrick[height, width];
IBrick[,] oldShapeArray = new IBrick[width, height];
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
newShapeArray[height - row - 1, column] = shapeArray[column, row];
oldShapeArray[column, row] = shapeArray[column, row];
}
}
containerBoard.RemovePieceFromCurrentPosition(this);
int w = width;
int h = height;
this.width = h;
this.height = w;
this.shapeArray = newShapeArray;
if (containerBoard.TestPieceOnPosition(this, this.X, this.Y))
{
containerBoard.PutPieceOnPosition(this, this.X, this.Y);
}
else
{
this.width = w;
this.height = h;
this.shapeArray = oldShapeArray;
containerBoard.PutPieceOnPosition(this, this.X, this.Y);
}
}
return test;
}
public bool Rotate270()
{
bool test = false;
if (!anchored)
{
if (containerBoard == null)
throw new NullContainerBoardException();
containerBoard.RemovePieceFromCurrentPosition(this);
IBrick[,] newShapeArray = new IBrick[height, width];
IBrick[,] oldShapeArray = new IBrick[width, height];
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
newShapeArray[row, width - column - 1] = shapeArray[column, row];
oldShapeArray[column, row] = shapeArray[column, row];
}
}
int w = width;
int h = height;
this.width = h;
this.height = w;
this.shapeArray = newShapeArray;
if (containerBoard.TestPieceOnPosition(this, this.X, this.Y))
{
containerBoard.PutPieceOnPosition(this, this.X, this.Y);
}
else
{
this.width = w;
this.height = h;
this.shapeArray = oldShapeArray;
containerBoard.PutPieceOnPosition(this, this.X, this.Y);
}
}
return test;
}
Bonus: Creating Funny Brick Animations
This part is not really necessary for the game, but I thought it would add something to the look and feel of the game. At first I was not happy enough with the square, static bricks, so I wanted to create some movements, so that the bricks appeared to be "shaking".
I managed to do this by creating a custom class, called ctlBrick
, that inherits from the Grid
class. Each ctlBrick
instance represents a different brick on the screen.
public void GenerateRandomPoints()
{
this.Children.Remove(path);
if (color != Colors.Transparent)
{
double h = this.Height;
double w = this.Width;
Random rnd = new Random();
p00 = new Point(2 + rnd.Next(-amplitude, amplitude), 2 + rnd.Next(-amplitude, amplitude));
p01 = new Point(2 + rnd.Next(-amplitude, amplitude), 1 * h / 4 + rnd.Next(-amplitude, amplitude));
p02 = new Point(2 + rnd.Next(-amplitude, amplitude), 3 * h / 4 + rnd.Next(-amplitude, amplitude));
p03 = new Point(2 + rnd.Next(-amplitude, amplitude), -2 + h + rnd.Next(-amplitude, amplitude));
p30 = new Point(-2 + w + rnd.Next(-amplitude, amplitude), 2 + rnd.Next(-amplitude, amplitude));
p31 = new Point(-2 + w + rnd.Next(-amplitude, amplitude), 1 * h / 4 + rnd.Next(-amplitude, amplitude));
p32 = new Point(-2 + w + rnd.Next(-amplitude, amplitude), 3 * h / 4 + rnd.Next(-amplitude, amplitude));
p33 = new Point(-2 + w + rnd.Next(-amplitude, amplitude), -2 + h + rnd.Next(-amplitude, amplitude));
p10 = new Point(1 * w / 4 + rnd.Next(-amplitude, amplitude), 2 + rnd.Next(-amplitude, amplitude));
p20 = new Point(3 * w / 4 + rnd.Next(-amplitude, amplitude), 2 + rnd.Next(-amplitude, amplitude));
p13 = new Point(1 * w / 4 + rnd.Next(-amplitude, amplitude), -2 + h + rnd.Next(-amplitude, amplitude));
p23 = new Point(3 * w / 4 + rnd.Next(-amplitude, amplitude), -2 + h + rnd.Next(-amplitude, amplitude));
var figures = new PathFigureCollection();
var pathSegmentCollection1 = new PathSegmentCollection();
var pathSegmentCollection2 = new PathSegmentCollection();
var pathSegmentCollection3 = new PathSegmentCollection();
var pathSegmentCollection4 = new PathSegmentCollection();
PointCollection pointCollection = new PointCollection();
pointCollection.Add(p10);
pointCollection.Add(p20);
pointCollection.Add(p30);
pointCollection.Add(p31);
pointCollection.Add(p32);
pointCollection.Add(p33);
pointCollection.Add(p23);
pointCollection.Add(p13);
pointCollection.Add(p03);
pointCollection.Add(p02);
pointCollection.Add(p01);
pointCollection.Add(p00);
pathSegmentCollection4.Add(new PolyBezierSegment() { Points = pointCollection });
figures.Add(new PathFigure() { StartPoint = p00, Segments = pathSegmentCollection4, IsClosed = true });
path = new Path()
{
Data = new PathGeometry()
{
Figures = figures
},
Stroke = new SolidColorBrush(Colors.Black),
StrokeThickness = 2,
Fill = new SolidColorBrush(color)
};
this.Children.Add(path);
}
}
The code above is what gives each brick a kind of "shaking" appearance. The "shaking" square is actually composed by aPath
element, which contains a PolyBezierSegment
class that involves the Grid
element. The PolyBezierSegment
as shown above defines a collection of random points that roughly imitate a square, but that slightly moves left, right, up and down, giving an impression of a "shaking square".
Bonus: Templating Funny Buttons
Another nice feature in Silverlight is the one that allows you to create templates for standard visual elements, such as buttons.
In our case, the standard button element doesn't look so good for the other elements on the screen. We have colorful bricks and the funny Comic Sans typeface, so the standard Silverlight button just doesn't fit in. But fortunately we can work on the templates, so that the regular buttons can share the same look and feel of the rest of the application. Here's how it works:
- First of all, we must define the setters for the regular button properties:
<Style TargetType="Button">
<Setter Property="FontFamily" Value="Comic Sans MS"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="#FF000000"/>
<Setter Property="Padding" Value="3"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush">
<Setter.Value>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFA3AEB9" Offset="0"/>
<GradientStop Color="#FF8399A9" Offset="0.375"/>
<GradientStop Color="#FF718597" Offset="0.375"/>
<GradientStop Color="#FF617584" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
- Then we must define a setter for a the
Template
property:
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid ShowGridLines="False">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<!--When the mouse is over the button, the button background is highlighted. -->
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient"
Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)"
To="#FFF0F0F0"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient"
Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)"
To="#FFF0F0F0"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient"
Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)"
To="#FFF0F000"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient"
Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)"
To="#FFF0F000"/>
</Storyboard>
</VisualState>
<!--When the button is pressed, the button gets a blue bold border. -->
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Duration="0"
Storyboard.TargetName="BackgroundGradient"
Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)"
To="#FFFFFFFF"/>
<ColorAnimation Duration="0"
Storyboard.TargetName="BackgroundGradient"
Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)"
To="#FFF0F0F0"/>
<ColorAnimation Duration="0"
Storyboard.TargetName="BackgroundGradient"
Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)"
To="#FFE0E000"/>
<ColorAnimation Duration="0"
Storyboard.TargetName="BackgroundGradient"
Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)"
To="#FFFFFFFF"/>
</Storyboard>
</VisualState>
<!--When the button is disabled, the button's opacity is lowered to a bit more than 50%. -->
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Duration="0"
Storyboard.TargetName="DisabledVisualElement"
Storyboard.TargetProperty="Opacity" To=".55"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
<!--When the button is focused, the button's FocusVisualElement's opacity is set to 100%. -->
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<Storyboard>
<DoubleAnimation Duration="0"
Storyboard.TargetName="FocusVisualElement"
Storyboard.TargetProperty="Opacity" To="1"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Unfocused" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="Background"
Grid.Column="1" Margin="0,5,0,0" CornerRadius="0"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<Grid Margin="1" ShowGridLines="False">
<Path Stroke="Blue"
StrokeThickness="2" x:Name="BackgroundGradient">
<Path.Data>
<PathGeometry>
<PathFigureCollection>
<PathFigure StartPoint="10,0" IsClosed="True">
<PathFigure.Segments>
<PolyBezierSegment Points="
30,5 50,-5 75,0
80,10 80,20 75,30
50,25 30,35 10,30
5,20 5,10 10,0"/>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry>
</Path.Data>
<Path.Fill>
<LinearGradientBrush x:Name="BackgroundAnimation"
StartPoint=".7,0" EndPoint=".7,1" Opacity="1" >
<GradientStop Color="#FFF0F0F0" Offset="0"/>
<GradientStop Color="#FFF0F0F0" Offset="0.5"/>
<GradientStop Color="#FFC0C000" Offset="0.5"/>
<GradientStop Color="#FFC0C000" Offset="1"/>
</LinearGradientBrush>
</Path.Fill>
</Path>
</Grid>
</Border>
<ContentPresenter
x:Name="contentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
Grid.Column="1"
/>
<Rectangle x:Name="DisabledVisualElement"
Grid.Column="1" RadiusX="0" RadiusY="3"
Fill="#FFFFFFFF" Opacity="0" IsHitTestVisible="false" />
<Path Grid.Column="1" StrokeThickness="3"
x:Name="FocusVisualElement"
Margin="0,5,0,0" Opacity="0"
IsHitTestVisible="false">
<Path.Stroke>
<LinearGradientBrush>
<GradientStop Color="Blue" Offset="0"/>
<GradientStop Color="Blue" Offset="1"/>
</LinearGradientBrush>
</Path.Stroke>
<Path.Data>
<PathGeometry>
<PathFigureCollection>
<PathFigure StartPoint="10,0" IsClosed="True">
<PathFigure.Segments>
<PolyBezierSegment
Points="30,5
50,-5 75,0
80,10 80,20 75,30
50,25 30,35 10,30
5,20 5,10 10,0"/>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry>
</Path.Data>
</Path>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
In the Normal state, the button's border is defined by a Path
element, which is defined by aPolyBezierSegment
described by the curvy lines you see here.
In the Focused state, the button gets a blue bold border, so that you can see which button is focused.
In the MouseOver state, the button's background is highlighted.
In the Disabled state, the button's opacity is lowered.
Hiç yorum yok:
Yorum Gönder