WPF Simple Puzzle
Developing a drag and drop technique in a simple puzzle game application.
Introduction
This article discusses a simple puzzle game which consists of 9 pieces, implemented with Windows Presentation Foundation. This article shows that drag and drop is not a difficult thing to implement in Windows Presentation Foundation.
Background
I am very grateful to Mr. Rudi Grobler who wrote the article "ListBox Drag & Drop Using Attached Properties". The code used in this application is based on the source code from his article. I am also very grateful to Mr. Seshi who write the article "8 Puzzle - WPF" whose application UI inspired this application's UI.
Using the Code
Classes in this application are:
Puzzle
puzzlePiece
: a collection which represents the puzzle pieces.name
Puzzle
: constructor.OnEdit
: method to raise the event.Initialize
: method to load all the puzzle pieces and shuffle them.Validate
: validate the puzzle.Edited
: event raised for each when a puzzle's arrangement is edited.PuzzlePiece
index
: the index of the piece.PuzzleImageSource
UriString
DragFrom
: to indicate whether the puzzle piece drags fromListBox
/Canvas
.MainWindow
(UI)puzzle
: represents the puzzle used in this application. The puzzle itself consists of apuzzlePiece
, which is of typeObservableCollection
and represents the puzzle pieces that must be arranged;name
, which is the puzzle name, and an eventEdited
that is raised whenever the puzzle is edited.itemPlacement
: represents the placement of the puzzle pieces. It is used to validate the puzzle. It is a collection of puzzle pieces whose index represent theCanvas
' index, and it contains the puzzle piece dropped in theCanvas
itself.emptyItem
: represents an empty puzzle item that indicates if aCanvas
doesn't contain a puzzle item.lbDragSource
: AListBox
object to refer to theListBox
that raises the drag.cvDragSource
: ACanvas
object to refer to theCanvas
that raises the drag.MainWindow()
: constructor.puzzleItemList_PreviewMouseLeftButtonDown
: handles a drag attempt toListBox
.PzItmCvs_MouseLeftButtonDown
: handles a drag attempt toCanvas
.PuzzleItemDrop
: handles the drop toCanvas
.puzzle_Edited
: handles the edited event of the puzzle.GetDataFromCanvas
: gets data which will be transferred via drag drop if the drag is from aCanvas
.GetObjectDataFromPoint
: gets data which will be transferred via drag drop if the drag is from aListBox
.instruction_Click
Attributes and objects
Methods:
Events:
Attributes and objects:
Attributes and objects:
Method:
Application Scenario
The application's steps are:
- The puzzle pieces are loaded into the listbox.
- The player drags a puzzle piece from the
ListBox
on the left and drops it to one of the nineCanvas
es in the right. The player may also drag a puzzle piece from theCanvas
to the otherCanvas
. If the destinationCanvas
is empty, the puzzle piece will be moved. If it is not empty, the puzzle piece will be exchanged (see the screenshot). - For each puzzle edited, an event is raised and the puzzle will be validated. If the puzzle is correctly arranged, the player wins.
Screenshot
This application uses a very simple user interface, as shown in the figure below. In the left side, there is a ListBox
where the puzzle pieces are located. The puzzle piece will be dragged to one of the Canvas
es on the right side. A puzzle piece may be exchanged with another piece too by dragging and dropping it to a puzzle piece in another Canvas
.
Initialization
Firstly, the puzzle piece is loaded. This application can use more than one kind of puzzle. An argument passed to the Initialize
method is used to determine what puzzle is used. It can be generated randomly, but in this application, there is only one built-in puzzle, the rabbit puzzle. So the value 1 is passed as the method call argument. Originally, the initialization is sorted. So we must shuffle the puzzle after it has been initialized. This is done with the help of a random number built by .NET.
public void Initialize(int chosen)
{
string directorySource = "";
if (chosen == 1)
{
this.name = "Rabbit Puzzle";
directorySource = "RabbitPuzzle";
}
for(int i=0; i<9; i++)
{
this.puzzlePiece.Add(new PuzzlePiece());
this.puzzlePiece[i].index = i;
this.puzzlePiece[i].UriString =
"Puzzle/" + directorySource + "/" + (i + 1).ToString() + ".png";
this.puzzlePiece[i].PuzzleImageSource =
new BitmapImage(new Uri(this.puzzlePiece[i].UriString, UriKind.Relative));
}
//shuffle
Random rand = new Random();
for (int i = 0; i < 9; i++)
{
int random = rand.Next(0, 8);
PuzzlePiece buffer;
buffer = this.puzzlePiece[i];
this.puzzlePiece[i] = this.puzzlePiece[random];
this.puzzlePiece[random] = buffer;
}
}
The collection object itemPlacement
in the MainWindow
class is used to map the Canvas
and the puzzle piece contained. The default value will be emptyItem
, which is an object with a certain value defined in the constructor. Then the ListBox
's item source is set with the puzzle piece and the edited handler defined.
Dragging
There are two elements that can be dragged in this application: ListBox
and the Canvas
. Both of them use the PreviewMouseLeftButtonDown
.
In the PreviewMouseLeftButtonDown
for the listbox, the data transferred through drag and drop operation is from the method GetObjectDataFromPoint
. This method uses the drag source and the mouse coordinate relative from the drag source as arguments. So, what's inside the method GetObjectDataFromPoint
? That's the method from Mr. Rudi Grobler from his article: ListBox Drag & Drop Using Attached Properties (see http://www.codeproject.com/KB/WPF/WPFDragDrop.aspx). We can guess how the code inside the method works by modifying it this way:
private object GetObjectDataFromPoint(ListBox dragSource, Point point)
{
UIElement element = dragSource.InputHitTest(point) as UIElement;
//MessageBox.Show("Drag Source Element : " + element.ToString());
if (element != null)
{
object data = DependencyProperty.UnsetValue;
while (data == DependencyProperty.UnsetValue)
{
data = dragSource.ItemContainerGenerator.ItemFromContainer(element);
if (data == DependencyProperty.UnsetValue)
{
element = VisualTreeHelper.GetParent(element) as UIElement;
//MessageBox.Show("Element passed through : " + element.ToString());
}
if (element == dragSource)
{
return null;
//MessageBox.Show("element == dragSource");
}
}
if (data != DependencyProperty.UnsetValue)
{
//MessageBox.Show("Data : " + data.ToString());
return data;
}
}
return null;
}
Just uncomment the statement MessageBox.Show(...)
and run the application. From the image element which is dragged, iteratively seek its parent with the while
statement, until it reaches the ListBoxItem
. From the ListBoxItem
, we get the data from the method ItemFromContainer
. The data is an object of type PuzzlePiece
.
The PreviewMouseLeftButtonDown
for the Canvas
is identical, with a little modification to get the data from the Canvas
and doing a drag and drop operation. The difference is, in the Canvas
, we get the data directly from the itemPlacement
because in the Canvas
control, there is only the image, not the whole of the PuzzlePiece
object.
Dropping
There are four possible conditions in dropping:
- If the
Canvas
is empty and the puzzle piece is dragged from theListBox
, the puzzle piece will be dropped. - If the
Canvas
is empty and the puzzle piece is dragged from the otherCanvas
, the puzzle item will be moved there. - If the
Canvas
is not empty and the puzzle piece is dragged from theListBox
, the puzzle item won't be dropped. - If the
Canvas
is not empty (there is another puzzle piece) and the puzzle piece is dragged from the otherCanvas
, those two puzzle pieces will be exchanged.
When a puzzle piece is dropped, there are several things to do: drop it in the appropriate Canvas
, update the puzzle item placement, remove that item from the source (ListBox
/ Canvas
) if it is not switched, check if the puzzle is valid, and delete the value of the DragFrom
property. For conditions 1 and 2, the process is simple; first, we check if the Children
property of the Canvas
is 0 (it means it has no child element). Then, we define an image control whose width, height, and source is the image source's width and height, and the image source we got from the puzzle piece data is transferred from the drag operation. After that, the old puzzle piece is deleted and the placement is updated.
Image imageControl = new Image()
{
Width = destination.Width,
Height = destination.Height,
Source = itemTransferred.PuzzleImageSource,
Stretch = Stretch.UniformToFill
};
//For condition 1 a and b, canvas is empty
if (destination.Children.Count == 0)
{
//put the puzzle piece to the canvas
destination.Children.Add(imageControl);
//Step 2
//Update PuzzleItemPlacement
//get the placement index to be updated
int indexToUpdate = int.Parse(destination.Tag.ToString());
//update now
//this statement is for condition 1 a (item from listbox)
if (itemTransferred.DragFrom == typeof(ListBox))
{
//update
this.itemPlacement[indexToUpdate] = itemTransferred;
//Step 3
//delete the item dragged from listbox
//NOTE : DELETING this way makes puzzle
// pieces defined in puzzle.puzzleItem DELETED
((IList)lbDragSource.ItemsSource).Remove(itemTransferred);
}
For condition 3, we only have to return from the method. For condition 4, first, we must get both of the indices, the source and the destination index. This is because we must exchange both of the puzzle pieces, the piece from the Canvas
which is dragged and the Canvas
where the puzzle piece is dropped. To change the Image
control which is in the Canvas
, the associated Canvas
is accessed with the method GetAssociatedCanvasByIndex
. This method takes an integer argument which is the index of the Canvas
to be accessed, and will return the associated Canvas
.
else if (destination.Children.Count > 0)
{
//condition 1c, from listbox
if (itemTransferred.DragFrom == typeof(ListBox))
{
//do nothing
return;
}
//condition 1d
else if (itemTransferred.DragFrom == typeof(Canvas))
{
//Step 1 and 2, switch them
//get the previous and destination index
int sourceIndex = itemPlacement.IndexOf(itemTransferred);
int destinationIndex = int.Parse(destination.Tag.ToString());
Object buffer = null;
//switch the image
Image image0 = new Image() { Width = destination.Width,
Height = destination.Height, Stretch = Stretch.Fill };
image0.Source = itemPlacement[sourceIndex].PuzzleImageSource;
Image image1 = new Image() { Width = destination.Width,
Height = destination.Height, Stretch = Stretch.Fill };
image1.Source = itemPlacement[destinationIndex].PuzzleImageSource;
GetAssociatedCanvasByIndex(sourceIndex).Children.Clear();
GetAssociatedCanvasByIndex(destinationIndex).Children.Clear();
GetAssociatedCanvasByIndex(sourceIndex).Children.Add(image1);
GetAssociatedCanvasByIndex(destinationIndex).Children.Add(image0);
image0 = null;
image1 = null;
//switch the placement
buffer = itemPlacement[sourceIndex];
itemPlacement[sourceIndex] = itemPlacement[destinationIndex];
itemPlacement[destinationIndex] = buffer as PuzzlePiece;
buffer = null;
}
}
Conclusion
Corrections and suggestions are appreciated. I hope this article will help someone who is struggling with a drag and drop puzzle. I'm sorry for any mistakes I made in this article, and thank you very much.
Points of Interest
The difficult things in creating this application were: understanding the GetObjectDataFromPoint
method, and manually slicing the rabbit image with Microsoft Paint (believe me, that's difficult :D).
History
This is the first release of the puzzle application.